summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorClement Ho <ClemMakesApps@gmail.com>2017-12-21 09:42:28 -0600
committerClement Ho <ClemMakesApps@gmail.com>2017-12-21 09:42:28 -0600
commitc46f7b2a2620a5b90003b40727bb3c1705dd1a0f (patch)
tree5808b9617af781c8cfb614d11540bdefc872851e
parent0e50e9d9d48752a58b640064075f7786f86e7433 (diff)
parent450317298ce177722b721353a0161d4347536120 (diff)
downloadgitlab-ce-c46f7b2a2620a5b90003b40727bb3c1705dd1a0f.tar.gz
Merge branch 'master' into fix-webpack-chunk-naming
-rw-r--r--.gitlab-ci.yml78
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock4
-rw-r--r--app/assets/javascripts/api.js14
-rw-r--r--app/assets/javascripts/commit/image_file.js1
-rw-r--r--app/assets/javascripts/dispatcher.js5
-rw-r--r--app/assets/javascripts/docs/docs_bundle.js13
-rw-r--r--app/assets/javascripts/fly_out_nav.js17
-rw-r--r--app/assets/javascripts/gl_dropdown.js24
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors.js21
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js48
-rw-r--r--app/assets/javascripts/helpers/user_feature_helper.js7
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue66
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue (renamed from app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue)0
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue (renamed from app/assets/javascripts/repo/components/commit_sidebar/list_item.vue)0
-rw-r--r--app/assets/javascripts/ide/components/ide.vue73
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue75
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue66
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue62
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue71
-rw-r--r--app/assets/javascripts/ide/components/new_branch_form.vue (renamed from app/assets/javascripts/repo/components/new_branch_form.vue)2
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue101
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue (renamed from app/assets/javascripts/repo/components/new_dropdown/modal.vue)16
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue (renamed from app/assets/javascripts/repo/components/new_dropdown/upload.vue)35
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue (renamed from app/assets/javascripts/repo/components/repo_commit_section.vue)45
-rw-r--r--app/assets/javascripts/ide/components/repo_edit_button.vue (renamed from app/assets/javascripts/repo/components/repo_edit_button.vue)0
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue (renamed from app/assets/javascripts/repo/components/repo_editor.vue)42
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue (renamed from app/assets/javascripts/repo/components/repo_file.vue)74
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue (renamed from app/assets/javascripts/repo/components/repo_file_buttons.vue)0
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue (renamed from app/assets/javascripts/repo/components/repo_loading_file.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_prev_directory.vue (renamed from app/assets/javascripts/repo/components/repo_prev_directory.vue)8
-rw-r--r--app/assets/javascripts/ide/components/repo_preview.vue (renamed from app/assets/javascripts/repo/components/repo_preview.vue)0
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue (renamed from app/assets/javascripts/repo/components/repo_tab.vue)6
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue (renamed from app/assets/javascripts/repo/components/repo_tabs.vue)0
-rw-r--r--app/assets/javascripts/ide/ide_router.js101
-rw-r--r--app/assets/javascripts/ide/index.js55
-rw-r--r--app/assets/javascripts/ide/lib/common/disposable.js (renamed from app/assets/javascripts/repo/lib/common/disposable.js)0
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js (renamed from app/assets/javascripts/repo/lib/common/model.js)8
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js (renamed from app/assets/javascripts/repo/lib/common/model_manager.js)0
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js (renamed from app/assets/javascripts/repo/lib/decorations/controller.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js (renamed from app/assets/javascripts/repo/lib/diff/controller.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js (renamed from app/assets/javascripts/repo/lib/diff/diff.js)0
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js (renamed from app/assets/javascripts/repo/lib/diff/diff_worker.js)0
-rw-r--r--app/assets/javascripts/ide/lib/editor.js (renamed from app/assets/javascripts/repo/lib/editor.js)30
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js (renamed from app/assets/javascripts/repo/lib/editor_options.js)0
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js (renamed from app/assets/javascripts/repo/monaco_loader.js)0
-rw-r--r--app/assets/javascripts/ide/services/index.js (renamed from app/assets/javascripts/repo/services/index.js)7
-rw-r--r--app/assets/javascripts/ide/stores/actions.js179
-rw-r--r--app/assets/javascripts/ide/stores/actions/branch.js43
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js (renamed from app/assets/javascripts/repo/stores/actions/file.js)41
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js25
-rw-r--r--app/assets/javascripts/ide/stores/actions/tree.js188
-rw-r--r--app/assets/javascripts/ide/stores/getters.js19
-rw-r--r--app/assets/javascripts/ide/stores/index.js (renamed from app/assets/javascripts/repo/stores/index.js)0
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js (renamed from app/assets/javascripts/repo/stores/mutation_types.js)19
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js (renamed from app/assets/javascripts/repo/stores/mutations.js)18
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js28
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js (renamed from app/assets/javascripts/repo/stores/mutations/file.js)20
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js (renamed from app/assets/javascripts/repo/stores/mutations/tree.js)9
-rw-r--r--app/assets/javascripts/ide/stores/state.js (renamed from app/assets/javascripts/repo/stores/state.js)20
-rw-r--r--app/assets/javascripts/ide/stores/utils.js (renamed from app/assets/javascripts/repo/stores/utils.js)64
-rw-r--r--app/assets/javascripts/issue_show/components/app.vue2
-rw-r--r--app/assets/javascripts/issue_show/components/title.vue2
-rw-r--r--app/assets/javascripts/issue_show/index.js7
-rw-r--r--app/assets/javascripts/layout_nav.js70
-rw-r--r--app/assets/javascripts/lib/utils/datetime_utility.js18
-rw-r--r--app/assets/javascripts/main.js5
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue24
-rw-r--r--app/assets/javascripts/monitoring/utils/date_time_formatters.js43
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js21
-rw-r--r--app/assets/javascripts/new_commit_form.js7
-rw-r--r--app/assets/javascripts/preview_markdown.js6
-rw-r--r--app/assets/javascripts/repo/components/commit_sidebar/list.vue89
-rw-r--r--app/assets/javascripts/repo/components/new_dropdown/index.vue89
-rw-r--r--app/assets/javascripts/repo/components/repo.vue63
-rw-r--r--app/assets/javascripts/repo/components/repo_sidebar.vue85
-rw-r--r--app/assets/javascripts/repo/index.js106
-rw-r--r--app/assets/javascripts/repo/stores/actions.js146
-rw-r--r--app/assets/javascripts/repo/stores/actions/branch.js20
-rw-r--r--app/assets/javascripts/repo/stores/actions/tree.js163
-rw-r--r--app/assets/javascripts/repo/stores/getters.js40
-rw-r--r--app/assets/javascripts/repo/stores/mutations/branch.js9
-rw-r--r--app/assets/javascripts/shortcuts.js10
-rw-r--r--app/assets/javascripts/users/activity_calendar.js9
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/mr_widget_options.js2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js28
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/mr_widget_store.js5
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/stores/state_maps.js17
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/header.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/project_avatar/image.vue103
-rw-r--r--app/assets/stylesheets/framework/contextual-sidebar.scss7
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss19
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss31
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss5
-rw-r--r--app/assets/stylesheets/framework/variables.scss1
-rw-r--r--app/assets/stylesheets/pages/issuable.scss4
-rw-r--r--app/assets/stylesheets/pages/repo.scss239
-rw-r--r--app/assets/stylesheets/pages/search.scss14
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss3
-rw-r--r--app/controllers/autocomplete_controller.rb4
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/projects/blob_controller.rb5
-rw-r--r--app/controllers/projects/clusters/gcp_controller.rb1
-rw-r--r--app/controllers/projects/clusters/user_controller.rb1
-rw-r--r--app/controllers/projects/clusters_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/helpers/application_helper.rb2
-rw-r--r--app/helpers/blob_helper.rb43
-rw-r--r--app/helpers/clusters_helper.rb5
-rw-r--r--app/helpers/form_helper.rb2
-rw-r--r--app/helpers/issuables_helper.rb2
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/sorting_helper.rb10
-rw-r--r--app/models/blob_viewer/dependency_manager.rb13
-rw-r--r--app/models/blob_viewer/package_json.rb18
-rw-r--r--app/models/ci/pipeline.rb13
-rw-r--r--app/models/commit.rb20
-rw-r--r--app/models/concerns/blocks_json_serialization.rb16
-rw-r--r--app/models/concerns/time_trackable.rb2
-rw-r--r--app/models/identity.rb10
-rw-r--r--app/models/project.rb2
-rw-r--r--app/models/repository.rb28
-rw-r--r--app/models/user.rb1
-rw-r--r--app/services/files/base_service.rb14
-rw-r--r--app/services/files/delete_service.rb10
-rw-r--r--app/services/files/multi_service.rb13
-rw-r--r--app/services/files/update_service.rb21
-rw-r--r--app/services/projects/unlink_fork_service.rb2
-rw-r--r--app/services/quick_actions/interpret_service.rb4
-rw-r--r--app/views/admin/groups/index.html.haml18
-rw-r--r--app/views/events/event/_push.html.haml3
-rw-r--r--app/views/help/index.html.haml10
-rw-r--r--app/views/ide/index.html.haml12
-rw-r--r--app/views/layouts/nav_only.html.haml13
-rw-r--r--app/views/projects/_files.html.haml2
-rw-r--r--app/views/projects/_md_preview.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml1
-rw-r--r--app/views/projects/blob/show.html.haml15
-rw-r--r--app/views/projects/blob/viewers/_dependency_manager.html.haml2
-rw-r--r--app/views/projects/clusters/gcp/_form.html.haml3
-rw-r--r--app/views/projects/clusters/gcp/_show.html.haml5
-rw-r--r--app/views/projects/clusters/user/_form.html.haml3
-rw-r--r--app/views/projects/clusters/user/_show.html.haml4
-rw-r--r--app/views/projects/issues/show.html.haml5
-rw-r--r--app/views/projects/pipelines/_info.html.haml50
-rw-r--r--app/views/projects/tree/_old_tree_content.html.haml24
-rw-r--r--app/views/projects/tree/_old_tree_header.html.haml64
-rw-r--r--app/views/projects/tree/_tree_content.html.haml29
-rw-r--r--app/views/projects/tree/_tree_header.html.haml78
-rw-r--r--app/views/projects/tree/show.html.haml7
-rw-r--r--app/views/shared/_ref_switcher.html.haml2
-rw-r--r--app/views/shared/boards/components/_sidebar.html.haml3
-rw-r--r--app/views/shared/groups/_dropdown.html.haml5
-rw-r--r--app/views/shared/repo/_repo.html.haml13
-rw-r--r--app/workers/concerns/project_import_options.rb23
-rw-r--r--app/workers/concerns/project_start_import.rb1
-rw-r--r--app/workers/expire_pipeline_cache_worker.rb2
-rw-r--r--app/workers/repository_fork_worker.rb22
-rw-r--r--app/workers/repository_import_worker.rb19
-rw-r--r--changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml5
-rw-r--r--changelogs/unreleased/33028-event-tag-links.yml5
-rw-r--r--changelogs/unreleased/36020-private-npm-modules.yml5
-rw-r--r--changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml5
-rw-r--r--changelogs/unreleased/38318-search-merge-requests-with-api.yml5
-rw-r--r--changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml5
-rw-r--r--changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml5
-rw-r--r--changelogs/unreleased/40063-markdown-editor-improvements.yml5
-rw-r--r--changelogs/unreleased/bvl-fix-unlinking-with-lfs-objects.yml6
-rw-r--r--changelogs/unreleased/fix-docs-help-shortcut.yml5
-rw-r--r--changelogs/unreleased/fix-onion-skin-reenter.yml5
-rw-r--r--changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml5
-rw-r--r--changelogs/unreleased/remove-links-mr-empty-state.yml5
-rw-r--r--changelogs/unreleased/show-inline-edit-btn.yml5
-rw-r--r--changelogs/unreleased/zj-empty-repo-importer.yml5
-rw-r--r--config/routes.rb2
-rw-r--r--config/webpack.config.js9
-rw-r--r--db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb29
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/high_availability/gitlab.md23
-rw-r--r--doc/api/commits.md1
-rw-r--r--doc/api/merge_requests.md2
-rw-r--r--doc/api/repository_files.md1
-rw-r--r--doc/ci/docker/using_docker_build.md15
-rw-r--r--doc/ci/examples/browser_performance.md50
-rw-r--r--doc/development/doc_styleguide.md4
-rw-r--r--doc/development/fe_guide/axios.md2
-rw-r--r--features/steps/project/source/browse_files.rb11
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/api/time_tracking_endpoints.rb4
-rw-r--r--lib/api/v3/time_tracking_endpoints.rb4
-rw-r--r--lib/gitlab/git/commit.rb13
-rw-r--r--lib/gitlab/git/repository.rb11
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb9
-rw-r--r--lib/gitlab/github_import/importer/repository_importer.rb2
-rw-r--r--package.json10
-rw-r--r--qa/README.md9
-rw-r--r--qa/qa.rb2
-rw-r--r--qa/qa/factory/base.rb27
-rw-r--r--qa/qa/factory/dependency.rb38
-rw-r--r--qa/qa/factory/product.rb26
-rw-r--r--qa/qa/factory/repository/push.rb19
-rw-r--r--qa/qa/factory/resource/group.rb22
-rw-r--r--qa/qa/factory/resource/project.rb19
-rw-r--r--qa/qa/factory/resource/sandbox.rb16
-rw-r--r--qa/qa/page/group/show.rb2
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb15
-rw-r--r--qa/spec/factory/base_spec.rb88
-rw-r--r--qa/spec/factory/dependency_spec.rb59
-rw-r--r--qa/spec/factory/product_spec.rb44
-rwxr-xr-xscripts/gitaly-test-spawn3
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb13
-rw-r--r--spec/features/calendar_spec.rb2
-rw-r--r--spec/features/help_pages_spec.rb18
-rw-r--r--spec/features/issues/issue_detail_spec.rb2
-rw-r--r--spec/features/merge_requests/image_diff_notes_spec.rb (renamed from spec/features/merge_requests/image_diff_notes.rb)45
-rw-r--r--spec/features/merge_requests/user_posts_notes_spec.rb15
-rw-r--r--spec/features/milestone_spec.rb4
-rw-r--r--spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb8
-rw-r--r--spec/features/projects/issuable_templates_spec.rb8
-rw-r--r--spec/features/projects/ref_switcher_spec.rb78
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb26
-rw-r--r--spec/features/projects/tree/create_file_spec.rb16
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb9
-rw-r--r--spec/features/tags/master_views_tags_spec.rb5
-rw-r--r--spec/finders/group_descendants_finder_spec.rb35
-rw-r--r--spec/javascripts/graphs/stat_graph_contributors_graph_spec.js12
-rw-r--r--spec/javascripts/merge_request_tabs_spec.js14
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js4
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_item_spec.js2
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_spec.js29
-rw-r--r--spec/javascripts/repo/components/ide_context_bar_spec.js49
-rw-r--r--spec/javascripts/repo/components/ide_repo_tree_spec.js (renamed from spec/javascripts/repo/components/repo_sidebar_spec.js)23
-rw-r--r--spec/javascripts/repo/components/ide_side_bar_spec.js43
-rw-r--r--spec/javascripts/repo/components/ide_spec.js (renamed from spec/javascripts/repo/components/repo_spec.js)12
-rw-r--r--spec/javascripts/repo/components/new_branch_form_spec.js4
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js17
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js149
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js81
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js33
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js8
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js6
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js13
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js5
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js4
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js10
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js4
-rw-r--r--spec/javascripts/repo/helpers.js5
-rw-r--r--spec/javascripts/repo/lib/common/disposable_spec.js2
-rw-r--r--spec/javascripts/repo/lib/common/model_manager_spec.js4
-rw-r--r--spec/javascripts/repo/lib/common/model_spec.js4
-rw-r--r--spec/javascripts/repo/lib/decorations/controller_spec.js8
-rw-r--r--spec/javascripts/repo/lib/diff/controller_spec.js12
-rw-r--r--spec/javascripts/repo/lib/diff/diff_spec.js2
-rw-r--r--spec/javascripts/repo/lib/editor_options_spec.js2
-rw-r--r--spec/javascripts/repo/lib/editor_spec.js4
-rw-r--r--spec/javascripts/repo/monaco_loader_spec.js2
-rw-r--r--spec/javascripts/repo/stores/actions/branch_spec.js20
-rw-r--r--spec/javascripts/repo/stores/actions/file_spec.js75
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js326
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js95
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js38
-rw-r--r--spec/javascripts/repo/stores/mutations/branch_spec.js6
-rw-r--r--spec/javascripts/repo/stores/mutations/file_spec.js4
-rw-r--r--spec/javascripts/repo/stores/mutations/tree_spec.js4
-rw-r--r--spec/javascripts/repo/stores/mutations_spec.js40
-rw-r--r--spec/javascripts/repo/stores/utils_spec.js43
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js28
-rw-r--r--spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js13
-rw-r--r--spec/lib/gitlab/git/gitlab_projects_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/importer/repository_importer_spec.rb8
-rw-r--r--spec/models/blob_viewer/package_json_spec.rb47
-rw-r--r--spec/models/commit_spec.rb39
-rw-r--r--spec/models/concerns/blocks_json_serialization_spec.rb17
-rw-r--r--spec/models/concerns/issuable_spec.rb4
-rw-r--r--spec/models/concerns/milestoneish_spec.rb4
-rw-r--r--spec/models/identity_spec.rb27
-rw-r--r--spec/models/project_spec.rb5
-rw-r--r--spec/models/repository_spec.rb65
-rw-r--r--spec/models/user_spec.rb1
-rw-r--r--spec/requests/api/merge_requests_spec.rb20
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb6
-rw-r--r--spec/services/files/delete_service_spec.rb64
-rw-r--r--spec/services/files/multi_service_spec.rb144
-rw-r--r--spec/services/projects/unlink_fork_service_spec.rb20
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb8
-rw-r--r--spec/services/system_note_service_spec.rb4
-rw-r--r--spec/support/api/time_tracking_shared_examples.rb6
-rw-r--r--spec/support/api/v3/time_tracking_shared_examples.rb6
-rw-r--r--spec/views/events/event/_push.html.haml_spec.rb55
-rw-r--r--spec/workers/concerns/project_import_options_spec.rb40
-rw-r--r--spec/workers/repository_fork_worker_spec.rb29
-rw-r--r--spec/workers/repository_import_worker_spec.rb23
-rw-r--r--yarn.lock110
299 files changed, 4831 insertions, 2357 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index d35fd28c766..c26e7f0aeba 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,5 +1,10 @@
image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.5-golang-1.8-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+.dedicated-runner: &dedicated-runner
+ retry: 1
+ tags:
+ - gitlab-org
+
.default-cache: &default-cache
key: "ruby-235-with-yarn"
paths:
@@ -42,11 +47,6 @@ stages:
- post-cleanup
# Predefined scopes
-.dedicated-runner: &dedicated-runner
- retry: 1
- tags:
- - gitlab-org
-
.tests-metadata-state: &tests-metadata-state
<<: *dedicated-runner
variables:
@@ -80,11 +80,15 @@ stages:
except:
- /(^qa[\/-].*|.*-qa$)/
+.except-docs-and-qa: &except-docs-and-qa
+ except:
+ - /(^docs[\/-].*|.*-docs$)/
+ - /(^qa[\/-].*|.*-qa$)/
+
.rspec-metadata: &rspec-metadata
<<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
- <<: *except-qa
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -121,9 +125,8 @@ stages:
.spinach-metadata: &spinach-metadata
<<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
- <<: *except-qa
stage: test
script:
- JOB_NAME=( $CI_JOB_NAME )
@@ -162,6 +165,7 @@ stages:
# Trigger a package build in omnibus-gitlab repository
#
package-qa:
+ <<: *dedicated-runner
image: ruby:2.4-alpine
before_script: []
stage: build
@@ -175,6 +179,7 @@ package-qa:
# Review docs base
.review-docs: &review-docs
+ <<: *dedicated-runner
<<: *except-qa
image: ruby:2.4-alpine
before_script:
@@ -220,8 +225,7 @@ review-docs-cleanup:
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
<<: *tests-metadata-state
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
stage: prepare
cache:
key: tests_metadata
@@ -284,9 +288,9 @@ flaky-examples-check:
- scripts/detect-new-flaky-examples $NEW_FLAKY_SPECS_REPORT
setup-test-env:
- <<: *use-pg
<<: *dedicated-runner
<<: *except-docs
+ <<: *use-pg
stage: prepare
cache:
<<: *default-cache
@@ -375,19 +379,18 @@ spinach-mysql 3 4: *spinach-metadata-mysql
SETUP_DB: "false"
.rake-exec: &rake-exec
- <<: *ruby-static-analysis
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
+ <<: *ruby-static-analysis
stage: test
script:
- bundle exec rake $CI_JOB_NAME
static-analysis:
- <<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
+ <<: *ruby-static-analysis
stage: test
script:
- scripts/static-analysis
@@ -441,8 +444,7 @@ ee_compat_check:
# DB migration, rollback, and seed jobs
.db-migrate-reset: &db-migrate-reset
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
script:
@@ -456,11 +458,16 @@ db:migrate:reset-mysql:
<<: *db-migrate-reset
<<: *use-mysql
+db:check-schema-pg:
+ <<: *db-migrate-reset
+ <<: *use-pg
+ script:
+ - source scripts/schema_changed.sh
+
.migration-paths: &migration-paths
<<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
- <<: *except-qa
stage: test
variables:
SETUP_DB: "false"
@@ -486,8 +493,7 @@ migration:path-mysql:
.db-rollback: &db-rollback
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
script:
@@ -504,8 +510,7 @@ db:rollback-mysql:
.db-seed_fu: &db-seed_fu
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
variables:
@@ -530,17 +535,10 @@ db:seed_fu-mysql:
<<: *db-seed_fu
<<: *use-mysql
-db:check-schema-pg:
- <<: *db-migrate-reset
- <<: *use-pg
- script:
- - source scripts/schema_changed.sh
-
# Frontend-related jobs
gitlab:assets:compile:
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: test
dependencies: []
@@ -561,11 +559,10 @@ gitlab:assets:compile:
- webpack-report/
karma:
- <<: *use-pg
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
+ <<: *use-pg
stage: test
variables:
BABEL_ENV: "coverage"
@@ -604,6 +601,7 @@ codequality:
paths: [codeclimate.json]
qa:internal:
+ <<: *dedicated-runner
<<: *except-docs
stage: test
variables:
@@ -616,8 +614,7 @@ qa:internal:
coverage:
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: post-test
services: []
@@ -636,8 +633,7 @@ coverage:
lint:javascript:report:
<<: *dedicated-runner
- <<: *except-docs
- <<: *except-qa
+ <<: *except-docs-and-qa
<<: *pull-cache
stage: post-test
dependencies:
@@ -695,9 +691,9 @@ cache gems:
- master@gitlab-org/gitlab-ee
gitlab_git_test:
+ <<: *dedicated-runner
+ <<: *except-docs-and-qa
<<: *pull-cache
- <<: *except-docs
- <<: *except-qa
variables:
SETUP_DB: "false"
script:
diff --git a/Gemfile b/Gemfile
index 6b1c6e16851..b6ffaf80f24 100644
--- a/Gemfile
+++ b/Gemfile
@@ -263,7 +263,7 @@ gem 'gettext_i18n_rails', '~> 1.8.0'
gem 'gettext_i18n_rails_js', '~> 1.2.0'
gem 'gettext', '~> 3.2.2', require: false, group: :development
-gem 'batch-loader'
+gem 'batch-loader', '~> 1.2.1'
# Perf bar
gem 'peek', '~> 1.0.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 11040fab805..a6e3c9e27cc 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -78,7 +78,7 @@ GEM
thread_safe (~> 0.3, >= 0.3.1)
babosa (1.0.2)
base32 (0.3.2)
- batch-loader (1.1.1)
+ batch-loader (1.2.1)
bcrypt (3.1.11)
bcrypt_pbkdf (1.0.0)
benchmark-ips (2.3.0)
@@ -988,7 +988,7 @@ DEPENDENCIES
awesome_print (~> 1.2.0)
babosa (~> 1.0.2)
base32 (~> 0.3.0)
- batch-loader
+ batch-loader (~> 1.2.1)
bcrypt_pbkdf (~> 1.0)
benchmark-ips (~> 2.3.0)
better_errors (~> 2.1.0)
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index d963101028a..21d8c790e90 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import axios from './lib/utils/axios_utils';
const Api = {
groupsPath: '/api/:version/groups.json',
@@ -6,6 +7,7 @@ const Api = {
namespacesPath: '/api/:version/namespaces.json',
groupProjectsPath: '/api/:version/groups/:id/projects.json',
projectsPath: '/api/:version/projects.json',
+ projectPath: '/api/:version/projects/:id',
projectLabelsPath: '/:namespace_path/:project_path/labels',
groupLabelsPath: '/groups/:namespace_path/labels',
licensePath: '/api/:version/templates/licenses/:key',
@@ -76,6 +78,14 @@ const Api = {
.done(projects => callback(projects));
},
+ // Return single project
+ project(projectPath) {
+ const url = Api.buildUrl(Api.projectPath)
+ .replace(':id', encodeURIComponent(projectPath));
+
+ return axios.get(url);
+ },
+
newLabel(namespacePath, projectPath, data, callback) {
let url;
@@ -115,7 +125,7 @@ const Api = {
commitMultiple(id, data) {
// 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);
+ .replace(':id', encodeURIComponent(id));
return this.wrapAjaxCall({
url,
type: 'POST',
@@ -127,7 +137,7 @@ const Api = {
branchSingle(id, branch) {
const url = Api.buildUrl(Api.branchSinglePath)
- .replace(':id', id)
+ .replace(':id', encodeURIComponent(id))
.replace(':branch', branch);
return this.wrapAjaxCall({
diff --git a/app/assets/javascripts/commit/image_file.js b/app/assets/javascripts/commit/image_file.js
index 5662802525e..b6a0ece7907 100644
--- a/app/assets/javascripts/commit/image_file.js
+++ b/app/assets/javascripts/commit/image_file.js
@@ -176,6 +176,7 @@ export default class ImageFile {
left: dragTrackWidth
});
+ $frameAdded.css('opacity', 1);
framePadding = parseInt($frameAdded.css('right').replace('px', ''), 10);
_this.initDraggable($dragger, framePadding, function(e, left) {
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 13fa553047a..118437b82a3 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -73,7 +73,6 @@ import initLegacyFilters from './init_legacy_filters';
import initIssuableSidebar from './init_issuable_sidebar';
import initProjectVisibilitySelector from './project_visibility';
import GpgBadges from './gpg_badges';
-import UserFeatureHelper from './helpers/user_feature_helper';
import initChangesDropdown from './init_changes_dropdown';
import NewGroupChild from './groups/new_group_child';
import AbuseReports from './abuse_reports';
@@ -449,9 +448,6 @@ import Activities from './activities';
break;
case 'projects:tree:show':
shortcut_handler = new ShortcutsNavigation();
-
- if (UserFeatureHelper.isNewRepoEnabled()) break;
-
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
@@ -470,7 +466,6 @@ import Activities from './activities';
shortcut_handler = true;
break;
case 'projects:blob:show':
- if (UserFeatureHelper.isNewRepoEnabled()) break;
new BlobViewer();
initBlob();
break;
diff --git a/app/assets/javascripts/docs/docs_bundle.js b/app/assets/javascripts/docs/docs_bundle.js
new file mode 100644
index 00000000000..a32bd6d0fc7
--- /dev/null
+++ b/app/assets/javascripts/docs/docs_bundle.js
@@ -0,0 +1,13 @@
+import Mousetrap from 'mousetrap';
+
+function addMousetrapClick(el, key) {
+ el.addEventListener('click', () => Mousetrap.trigger(key));
+}
+
+function domContentLoaded() {
+ addMousetrapClick(document.querySelector('.js-trigger-shortcut'), '?');
+ addMousetrapClick(document.querySelector('.js-trigger-search-bar'), 's');
+}
+
+document.addEventListener('DOMContentLoaded', domContentLoaded);
+
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 6110d961609..abb04d77f8f 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -161,13 +161,16 @@ export default () => {
const items = [...sidebar.querySelectorAll('.sidebar-top-level-items > li')];
- sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
- clearTimeout(timeoutId);
-
- timeoutId = setTimeout(() => {
- if (currentOpenMenu) hideMenu(currentOpenMenu);
- }, getHideSubItemsInterval());
- });
+ const topItems = sidebar.querySelector('.sidebar-top-level-items');
+ if (topItems) {
+ sidebar.querySelector('.sidebar-top-level-items').addEventListener('mouseleave', () => {
+ clearTimeout(timeoutId);
+
+ timeoutId = setTimeout(() => {
+ if (currentOpenMenu) hideMenu(currentOpenMenu);
+ }, getHideSubItemsInterval());
+ });
+ }
headerHeight = document.querySelector('.nav-sidebar').offsetTop;
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index cf4a70e321e..64f258aed64 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -300,7 +300,7 @@ GitLabDropdown = (function() {
return function(data) {
_this.fullData = data;
_this.parseData(_this.fullData);
- _this.focusTextInput(true);
+ _this.focusTextInput();
if (_this.options.filterable && _this.filter && _this.filter.input && _this.filter.input.val() && _this.filter.input.val().trim() !== '') {
return _this.filter.input.trigger('input');
}
@@ -790,24 +790,16 @@ GitLabDropdown = (function() {
return [selectedObject, isMarking];
};
- GitLabDropdown.prototype.focusTextInput = function(triggerFocus = false) {
+ GitLabDropdown.prototype.focusTextInput = function() {
if (this.options.filterable) {
- this.dropdown.one('transitionend', () => {
- const initialScrollTop = $(window).scrollTop();
+ const initialScrollTop = $(window).scrollTop();
- if (this.dropdown.is('.open')) {
- this.filterInput.focus();
- }
-
- if ($(window).scrollTop() < initialScrollTop) {
- $(window).scrollTop(initialScrollTop);
- }
- });
+ if (this.dropdown.is('.open')) {
+ this.filterInput.focus();
+ }
- if (triggerFocus) {
- // This triggers after a ajax request
- // in case of slow requests, the dropdown transition could already be finished
- this.dropdown.trigger('transitionend');
+ if ($(window).scrollTop() < initialScrollTop) {
+ $(window).scrollTop(initialScrollTop);
}
}
};
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors.js b/app/assets/javascripts/graphs/stat_graph_contributors.js
index 743c049e9fb..151a4ce012c 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors.js
@@ -84,9 +84,12 @@ export default (function() {
return _.each(author_commits, (function(_this) {
return function(d) {
_this.redraw_author_commit_info(d);
- $(_this.authors[d.author_name].list_item).appendTo("ol");
- _this.authors[d.author_name].set_data(d.dates);
- return _this.authors[d.author_name].redraw();
+ if (_this.authors[d.author_name] != null) {
+ $(_this.authors[d.author_name].list_item).appendTo("ol");
+ _this.authors[d.author_name].set_data(d.dates);
+ return _this.authors[d.author_name].redraw();
+ }
+ return '';
};
})(this));
};
@@ -108,10 +111,14 @@ export default (function() {
};
ContributorsStatGraph.prototype.redraw_author_commit_info = function(author) {
- var author_commit_info, author_list_item;
- author_list_item = $(this.authors[author.author_name].list_item);
- author_commit_info = this.format_author_commit_info(author);
- return author_list_item.find("span").html(author_commit_info);
+ var author_commit_info, author_list_item, $author;
+ $author = this.authors[author.author_name];
+ if ($author != null) {
+ author_list_item = $(this.authors[author.author_name].list_item);
+ author_commit_info = this.format_author_commit_info(author);
+ return author_list_item.find("span").html(author_commit_info);
+ }
+ return '';
};
return ContributorsStatGraph;
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
index 187f3c008e8..9a4012232a0 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js
@@ -1,8 +1,16 @@
/* eslint-disable func-names, space-before-function-paren, no-var, prefer-rest-params, max-len, no-restricted-syntax, vars-on-top, no-use-before-define, no-param-reassign, new-cap, no-underscore-dangle, wrap-iife, comma-dangle, no-return-assign, prefer-arrow-callback, quotes, prefer-template, newline-per-chained-call, no-else-return, no-shadow */
import _ from 'underscore';
-import d3 from 'd3';
+import { extent, max } from 'd3-array';
+import { select, event as d3Event } from 'd3-selection';
+import { scaleTime, scaleLinear } from 'd3-scale';
+import { axisLeft, axisBottom } from 'd3-axis';
+import { area } from 'd3-shape';
+import { brushX } from 'd3-brush';
+import { timeParse } from 'd3-time-format';
import { dateTickFormat } from '../lib/utils/tick_formats';
+const d3 = { extent, max, select, scaleTime, scaleLinear, axisLeft, axisBottom, area, brushX, timeParse };
+
const extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; };
const hasProp = {}.hasOwnProperty;
@@ -71,8 +79,8 @@ export const ContributorsGraph = (function() {
};
ContributorsGraph.prototype.create_scale = function(width, height) {
- this.x = d3.time.scale().range([0, width]).clamp(true);
- return this.y = d3.scale.linear().range([height, 0]).nice();
+ this.x = d3.scaleTime().range([0, width]).clamp(true);
+ return this.y = d3.scaleLinear().range([height, 0]).nice();
};
ContributorsGraph.prototype.draw_x_axis = function() {
@@ -124,7 +132,7 @@ export const ContributorsMasterGraph = (function(superClass) {
ContributorsMasterGraph.prototype.parse_dates = function(data) {
var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
+ parseDate = d3.timeParse("%Y-%m-%d");
return data.forEach(function(d) {
return d.date = parseDate(d.date);
});
@@ -135,11 +143,10 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis()
+ this.x_axis = d3.axisBottom()
.scale(this.x)
- .orient('bottom')
.tickFormat(dateTickFormat);
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
};
ContributorsMasterGraph.prototype.create_svg = function() {
@@ -147,16 +154,16 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
+ return this.area = d3.area().x(function(d) {
return x(d.date);
}).y0(this.height).y1(function(d) {
d.commits = d.commits || d.additions || d.deletions;
return y(d.commits);
- }).interpolate("basis");
+ });
};
ContributorsMasterGraph.prototype.create_brush = function() {
- return this.brush = d3.svg.brush().x(this.x).on("brushend", this.update_content);
+ return this.brush = d3.brushX(this.x).extent([[this.x.range()[0], 0], [this.x.range()[1], this.height]]).on("end", this.update_content);
};
ContributorsMasterGraph.prototype.draw_path = function(data) {
@@ -168,7 +175,12 @@ export const ContributorsMasterGraph = (function(superClass) {
};
ContributorsMasterGraph.prototype.update_content = function() {
- ContributorsGraph.set_x_domain(this.brush.empty() ? this.x_max_domain : this.brush.extent());
+ // d3Event.selection replaces the function brush.empty() calls
+ if (d3Event.selection != null) {
+ ContributorsGraph.set_x_domain(d3Event.selection.map(this.x.invert));
+ } else {
+ ContributorsGraph.set_x_domain(this.x_max_domain);
+ }
return $("#brush_change").trigger('change');
};
@@ -226,18 +238,17 @@ export const ContributorsAuthorGraph = (function(superClass) {
};
ContributorsAuthorGraph.prototype.create_axes = function() {
- this.x_axis = d3.svg.axis()
+ this.x_axis = d3.axisBottom()
.scale(this.x)
- .orient('bottom')
.ticks(8)
.tickFormat(dateTickFormat);
- return this.y_axis = d3.svg.axis().scale(this.y).orient("left").ticks(5);
+ return this.y_axis = d3.axisLeft().scale(this.y).ticks(5);
};
ContributorsAuthorGraph.prototype.create_area = function(x, y) {
- return this.area = d3.svg.area().x(function(d) {
+ return this.area = d3.area().x(function(d) {
var parseDate;
- parseDate = d3.time.format("%Y-%m-%d").parse;
+ parseDate = d3.timeParse("%Y-%m-%d");
return x(parseDate(d));
}).y0(this.height).y1((function(_this) {
return function(d) {
@@ -247,11 +258,12 @@ export const ContributorsAuthorGraph = (function(superClass) {
return y(0);
}
};
- })(this)).interpolate("basis");
+ })(this));
};
ContributorsAuthorGraph.prototype.create_svg = function() {
- this.list_item = d3.selectAll(".person")[0].pop();
+ var persons = document.querySelectorAll('.person');
+ this.list_item = persons[persons.length - 1];
return this.svg = d3.select(this.list_item).append("svg").attr("width", this.width + this.MARGIN.left + this.MARGIN.right).attr("height", this.height + this.MARGIN.top + this.MARGIN.bottom).attr("class", "spark").append("g").attr("transform", "translate(" + this.MARGIN.left + "," + this.MARGIN.top + ")");
};
diff --git a/app/assets/javascripts/helpers/user_feature_helper.js b/app/assets/javascripts/helpers/user_feature_helper.js
deleted file mode 100644
index 638118a5204..00000000000
--- a/app/assets/javascripts/helpers/user_feature_helper.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import Cookies from 'js-cookie';
-
-export default {
- isNewRepoEnabled() {
- return Cookies.get('new_repo') === 'true';
- },
-};
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
new file mode 100644
index 00000000000..704dff981df
--- /dev/null
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
@@ -0,0 +1,66 @@
+<script>
+ import { mapState } from 'vuex';
+ import icon from '../../../vue_shared/components/icon.vue';
+ import listItem from './list_item.vue';
+ import listCollapsed from './list_collapsed.vue';
+
+ export default {
+ components: {
+ icon,
+ listItem,
+ listCollapsed,
+ },
+ props: {
+ title: {
+ type: String,
+ required: true,
+ },
+ fileList: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
+ ]),
+ },
+ methods: {
+ toggleCollapsed() {
+ this.$emit('toggleCollapsed');
+ },
+ },
+
+ };
+</script>
+
+<template>
+ <div class="multi-file-commit-list">
+ <list-collapsed
+ v-if="rightPanelCollapsed"
+ />
+ <template v-else>
+ <ul
+ v-if="fileList.length"
+ class="list-unstyled append-bottom-0"
+ >
+ <li
+ v-for="file in fileList"
+ :key="file.key"
+ >
+ <list-item
+ :file="file"
+ />
+ </li>
+ </ul>
+ <div
+ v-else
+ class="help-block prepend-top-0"
+ >
+ No changes
+ </div>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
index 6a0262f271b..6a0262f271b 100644
--- a/app/assets/javascripts/repo/components/commit_sidebar/list_collapsed.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
index 742f746e02f..742f746e02f 100644
--- a/app/assets/javascripts/repo/components/commit_sidebar/list_item.vue
+++ b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
new file mode 100644
index 00000000000..7f29a355eca
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide.vue
@@ -0,0 +1,73 @@
+<script>
+import { mapState, mapGetters } from 'vuex';
+import ideSidebar from './ide_side_bar.vue';
+import ideContextbar from './ide_context_bar.vue';
+import repoTabs from './repo_tabs.vue';
+import repoFileButtons from './repo_file_buttons.vue';
+import ideStatusBar from './ide_status_bar.vue';
+import repoPreview from './repo_preview.vue';
+import repoEditor from './repo_editor.vue';
+
+export default {
+ computed: {
+ ...mapState([
+ 'currentBlobView',
+ 'selectedFile',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ 'activeFile',
+ ]),
+ },
+ components: {
+ ideSidebar,
+ ideContextbar,
+ repoTabs,
+ repoFileButtons,
+ ideStatusBar,
+ repoEditor,
+ repoPreview,
+ },
+ mounted() {
+ const returnValue = 'Are you sure you want to lose unsaved changes?';
+ window.onbeforeunload = (e) => {
+ if (!this.changedFiles.length) return undefined;
+
+ Object.assign(e, {
+ returnValue,
+ });
+ return returnValue;
+ };
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-view"
+ >
+ <ide-sidebar/>
+ <div
+ class="multi-file-edit-pane"
+ >
+ <template
+ v-if="activeFile">
+ <repo-tabs/>
+ <component
+ class="multi-file-edit-pane-content"
+ :is="currentBlobView"
+ />
+ <repo-file-buttons/>
+ <ide-status-bar
+ :file="selectedFile"/>
+ </template>
+ <template
+ v-else>
+ <div class="ide-empty-state">
+ <h2 class="clgray">Welcome to the GitLab IDE</h2>
+ </div>
+ </template>
+ </div>
+ <ide-contextbar/>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_context_bar.vue b/app/assets/javascripts/ide/components/ide_context_bar.vue
new file mode 100644
index 00000000000..5a08718e386
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_context_bar.vue
@@ -0,0 +1,75 @@
+<script>
+import { mapGetters, mapState, mapActions } from 'vuex';
+import repoCommitSection from './repo_commit_section.vue';
+import icon from '../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ repoCommitSection,
+ icon,
+ },
+ computed: {
+ ...mapState([
+ 'rightPanelCollapsed',
+ ]),
+ ...mapGetters([
+ 'changedFiles',
+ ]),
+ currentIcon() {
+ return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setPanelCollapsedStatus',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <div
+ class="multi-file-commit-panel-section">
+ <header
+ class="multi-file-commit-panel-header"
+ :class="{
+ 'is-collapsed': rightPanelCollapsed,
+ }"
+ >
+ <div
+ class="multi-file-commit-panel-header-title"
+ v-if="!rightPanelCollapsed">
+ <icon
+ name="list-bulleted"
+ :size="18"
+ />
+ Staged
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent multi-file-commit-panel-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ </button>
+ </header>
+ <repo-commit-section
+ class=""/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
new file mode 100644
index 00000000000..bd3a521ff43
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
@@ -0,0 +1,47 @@
+<script>
+import repoTree from './ide_repo_tree.vue';
+import icon from '../../vue_shared/components/icon.vue';
+import newDropdown from './new_dropdown/index.vue';
+
+export default {
+ components: {
+ repoTree,
+ icon,
+ newDropdown,
+ },
+ props: {
+ projectId: {
+ type: String,
+ required: true,
+ },
+ branch: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="branch-container">
+ <div class="branch-header">
+ <div class="branch-header-title">
+ <icon
+ name="branch"
+ :size="12">
+ </icon>
+ {{ branch.name }}
+ </div>
+ <div class="branch-header-btns">
+ <new-dropdown
+ :project-id="projectId"
+ :branch="branch.name"
+ path=""/>
+ </div>
+ </div>
+ <div>
+ <repo-tree
+ :treeId="branch.treeId"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_project_tree.vue b/app/assets/javascripts/ide/components/ide_project_tree.vue
new file mode 100644
index 00000000000..61daba6d176
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_project_tree.vue
@@ -0,0 +1,47 @@
+<script>
+import branchesTree from './ide_project_branches_tree.vue';
+import projectAvatarImage from '../../vue_shared/components/project_avatar/image.vue';
+
+export default {
+ components: {
+ branchesTree,
+ projectAvatarImage,
+ },
+ props: {
+ project: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="projects-sidebar">
+ <div class="context-header">
+ <a
+ :title="project.name"
+ :href="project.web_url">
+ <div class="avatar-container s40 project-avatar">
+ <project-avatar-image
+ class="avatar-container project-avatar"
+ :link-href="project.path"
+ :img-src="project.avatar_url"
+ :img-alt="project.name"
+ :img-size="40"
+ />
+ </div>
+ <div class="sidebar-context-title">
+ {{ project.name }}
+ </div>
+ </a>
+ </div>
+ <div class="multi-file-commit-panel-inner-scroll">
+ <branches-tree
+ v-for="(branch, index) in project.branches"
+ :key="branch.name"
+ :project-id="project.path_with_namespace"
+ :branch="branch"/>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_repo_tree.vue b/app/assets/javascripts/ide/components/ide_repo_tree.vue
new file mode 100644
index 00000000000..b6b089e6b25
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_repo_tree.vue
@@ -0,0 +1,66 @@
+<script>
+import { mapState } from 'vuex';
+import RepoPreviousDirectory from './repo_prev_directory.vue';
+import RepoFile from './repo_file.vue';
+import RepoLoadingFile from './repo_loading_file.vue';
+import { treeList } from '../stores/utils';
+
+export default {
+ components: {
+ 'repo-previous-directory': RepoPreviousDirectory,
+ 'repo-file': RepoFile,
+ 'repo-loading-file': RepoLoadingFile,
+ },
+ props: {
+ treeId: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'loading',
+ 'isRoot',
+ ]),
+ ...mapState({
+ projectName(state) {
+ return state.project.name;
+ },
+ }),
+ fetchedList() {
+ return treeList(this.$store.state, this.treeId);
+ },
+ hasPreviousDirectory() {
+ return !this.isRoot && this.fetchedList.length;
+ },
+ showLoading() {
+ return this.loading;
+ },
+ },
+};
+</script>
+
+<template>
+<div>
+ <div class="ide-file-list">
+ <table class="table">
+ <tbody
+ v-if="treeId">
+ <repo-previous-directory
+ v-if="hasPreviousDirectory"
+ />
+ <repo-loading-file
+ v-if="showLoading"
+ v-for="n in 5"
+ :key="n"
+ />
+ <repo-file
+ v-for="file in fetchedList"
+ :key="file.key"
+ :file="file"
+ />
+ </tbody>
+ </table>
+ </div>
+</div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_side_bar.vue b/app/assets/javascripts/ide/components/ide_side_bar.vue
new file mode 100644
index 00000000000..535398d98c2
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_side_bar.vue
@@ -0,0 +1,62 @@
+<script>
+import { mapState, mapActions } from 'vuex';
+import projectTree from './ide_project_tree.vue';
+import icon from '../../vue_shared/components/icon.vue';
+
+export default {
+ components: {
+ projectTree,
+ icon,
+ },
+ computed: {
+ ...mapState([
+ 'projects',
+ 'leftPanelCollapsed',
+ ]),
+ currentIcon() {
+ return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
+ },
+ },
+ methods: {
+ ...mapActions([
+ 'setPanelCollapsedStatus',
+ ]),
+ toggleCollapsed() {
+ this.setPanelCollapsedStatus({
+ side: 'left',
+ collapsed: !this.leftPanelCollapsed,
+ });
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ class="multi-file-commit-panel"
+ :class="{
+ 'is-collapsed': leftPanelCollapsed,
+ }"
+ >
+ <div class="multi-file-commit-panel-inner">
+ <project-tree
+ v-for="(project, index) in projects"
+ :key="project.id"
+ :project="project"/>
+ </div>
+ <button
+ type="button"
+ class="btn btn-transparent left-collapse-btn"
+ @click="toggleCollapsed"
+ >
+ <icon
+ :name="currentIcon"
+ :size="18"
+ />
+ <span
+ v-if="!leftPanelCollapsed"
+ class="collapse-text"
+ >Collapse sidebar</span>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
new file mode 100644
index 00000000000..a24abadd936
--- /dev/null
+++ b/app/assets/javascripts/ide/components/ide_status_bar.vue
@@ -0,0 +1,71 @@
+<script>
+import { mapState } from 'vuex';
+import icon from '../../vue_shared/components/icon.vue';
+import tooltip from '../../vue_shared/directives/tooltip';
+import timeAgoMixin from '../../vue_shared/mixins/timeago';
+
+export default {
+ props: {
+ file: {
+ type: Object,
+ required: true,
+ },
+ },
+ components: {
+ icon,
+ },
+ directives: {
+ tooltip,
+ },
+ mixins: [
+ timeAgoMixin,
+ ],
+ computed: {
+ ...mapState([
+ 'selectedFile',
+ ]),
+ },
+};
+</script>
+
+<template>
+ <div
+ class="ide-status-bar">
+ <div>
+ <icon
+ name="branch"
+ :size="12">
+ </icon>
+ {{ selectedFile.branchId }}
+ </div>
+ <div>
+ <div
+ v-if="selectedFile.lastCommit && selectedFile.lastCommit.id">
+ Last commit:
+ <a
+ v-tooltip
+ :title="selectedFile.lastCommit.message"
+ :href="selectedFile.lastCommit.url">
+ {{ timeFormated(selectedFile.lastCommit.updatedAt) }} by
+ {{ selectedFile.lastCommit.author }}
+ </a>
+ </div>
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.name }}
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.eol }}
+ </div>
+ <div
+ class="text-right">
+ {{ file.editorRow }}:{{ file.editorColumn }}
+ </div>
+ <div
+ class="text-right">
+ {{ selectedFile.fileLanguage }}
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue
index ba7090e4a9d..2119d373d31 100644
--- a/app/assets/javascripts/repo/components/new_branch_form.vue
+++ b/app/assets/javascripts/ide/components/new_branch_form.vue
@@ -44,7 +44,7 @@
this.branchName = '';
if (this.dropdownText) {
- this.dropdownText.textContent = this.currentBranch;
+ this.dropdownText.textContent = this.currentBranchId;
}
this.toggleDropdown();
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
new file mode 100644
index 00000000000..6e67e99a70f
--- /dev/null
+++ b/app/assets/javascripts/ide/components/new_dropdown/index.vue
@@ -0,0 +1,101 @@
+<script>
+ import newModal from './modal.vue';
+ import upload from './upload.vue';
+ import icon from '../../../vue_shared/components/icon.vue';
+
+ export default {
+ props: {
+ branch: {
+ type: String,
+ required: true,
+ },
+ path: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ default: null,
+ },
+ },
+ components: {
+ icon,
+ newModal,
+ upload,
+ },
+ data() {
+ return {
+ openModal: false,
+ modalType: '',
+ };
+ },
+ methods: {
+ createNewItem(type) {
+ this.modalType = type;
+ this.toggleModalOpen();
+ },
+ toggleModalOpen() {
+ this.openModal = !this.openModal;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="repo-new-btn pull-right">
+ <div class="dropdown">
+ <button
+ type="button"
+ class="btn btn-sm btn-default dropdown-toggle add-to-tree"
+ data-toggle="dropdown"
+ aria-label="Create new file or directory"
+ >
+ <icon
+ name="plus"
+ :size="12"
+ css-classes="pull-left"
+ />
+ <icon
+ name="arrow-down"
+ :size="12"
+ css-classes="pull-left"
+ />
+ </button>
+ <ul class="dropdown-menu dropdown-menu-right">
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('blob')"
+ >
+ {{ __('New file') }}
+ </a>
+ </li>
+ <li>
+ <upload
+ :branch-id="branch"
+ :path="path"
+ :parent="parent"
+ />
+ </li>
+ <li>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="createNewItem('tree')"
+ >
+ {{ __('New directory') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+ <new-modal
+ v-if="openModal"
+ :type="modalType"
+ :branch-id="branch"
+ :path="path"
+ :parent="parent"
+ @toggle="toggleModalOpen"
+ />
+ </div>
+</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
index c191af7dec3..a0650d37690 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/modal.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
@@ -1,10 +1,18 @@
<script>
- import { mapActions } from 'vuex';
+ import { mapActions, mapState } from 'vuex';
import { __ } from '../../../locale';
import modal from '../../../vue_shared/components/modal.vue';
export default {
props: {
+ branchId: {
+ type: String,
+ required: true,
+ },
+ parent: {
+ type: Object,
+ default: null,
+ },
type: {
type: String,
required: true,
@@ -28,6 +36,9 @@
]),
createEntryInStore() {
this.createTempEntry({
+ projectId: this.currentProjectId,
+ branchId: this.branchId,
+ parent: this.parent,
name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
type: this.type,
});
@@ -39,6 +50,9 @@
},
},
computed: {
+ ...mapState([
+ 'currentProjectId',
+ ]),
modalTitle() {
if (this.type === 'tree') {
return __('Create new directory');
diff --git a/app/assets/javascripts/repo/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
index 14ad32f4ae0..2a2f2a241fc 100644
--- a/app/assets/javascripts/repo/components/new_dropdown/upload.vue
+++ b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
@@ -1,12 +1,22 @@
<script>
- import { mapActions } from 'vuex';
+ import { mapActions, mapState } from 'vuex';
export default {
props: {
- path: {
+ branchId: {
type: String,
required: true,
},
+ parent: {
+ type: Object,
+ default: null,
+ },
+ },
+ computed: {
+ ...mapState([
+ 'trees',
+ 'currentProjectId',
+ ]),
},
methods: {
...mapActions([
@@ -22,6 +32,9 @@
this.createTempEntry({
name,
+ projectId: this.currentProjectId,
+ branchId: this.branchId,
+ parent: this.parent,
type: 'blob',
content: result,
base64: !isText,
@@ -42,6 +55,9 @@
openFile() {
Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
},
+ startFileUpload() {
+ this.$refs.fileUpload.click();
+ },
},
mounted() {
this.$refs.fileUpload.addEventListener('change', this.openFile);
@@ -53,16 +69,19 @@
</script>
<template>
- <label
- role="button"
- class="menu-item"
- >
- {{ __('Upload file') }}
+ <div>
+ <a
+ href="#"
+ role="button"
+ @click.prevent="startFileUpload"
+ >
+ {{ __('Upload file') }}
+ </a>
<input
id="file-upload"
type="file"
class="hidden"
ref="fileUpload"
/>
- </label>
+ </div>
</template>
diff --git a/app/assets/javascripts/repo/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
index 4e0178072cb..470db2c9650 100644
--- a/app/assets/javascripts/repo/components/repo_commit_section.vue
+++ b/app/assets/javascripts/ide/components/repo_commit_section.vue
@@ -20,12 +20,13 @@ export default {
submitCommitsLoading: false,
startNewMR: false,
commitMessage: '',
- collapsed: true,
};
},
computed: {
...mapState([
- 'currentBranch',
+ 'currentProjectId',
+ 'currentBranchId',
+ 'rightPanelCollapsed',
]),
...mapGetters([
'changedFiles',
@@ -42,12 +43,13 @@ export default {
'checkCommitStatus',
'commitChanges',
'getTreeData',
+ 'setPanelCollapsedStatus',
]),
makeCommit(newBranch = false) {
const createNewBranch = newBranch || this.startNewMR;
const payload = {
- branch: createNewBranch ? `${this.currentBranch}-${new Date().getTime().toString()}` : this.currentBranch,
+ branch: createNewBranch ? `${this.currentBranchId}-${new Date().getTime().toString()}` : this.currentBranchId,
commit_message: this.commitMessage,
actions: this.changedFiles.map(f => ({
action: f.tempFile ? 'create' : 'update',
@@ -55,7 +57,7 @@ export default {
content: f.content,
encoding: f.base64 ? 'base64' : 'text',
})),
- start_branch: createNewBranch ? this.currentBranch : undefined,
+ start_branch: createNewBranch ? this.currentBranchId : undefined,
};
this.showNewBranchModal = false;
@@ -64,7 +66,12 @@ export default {
this.commitChanges({ payload, newMr: this.startNewMR })
.then(() => {
this.submitCommitsLoading = false;
- this.getTreeData();
+ this.$store.dispatch('getTreeData', {
+ projectId: this.currentProjectId,
+ branch: this.currentBranchId,
+ endpoint: `/tree/${this.currentBranchId}`,
+ force: true,
+ });
})
.catch(() => {
this.submitCommitsLoading = false;
@@ -86,19 +93,17 @@ export default {
});
},
toggleCollapsed() {
- this.collapsed = !this.collapsed;
+ this.setPanelCollapsedStatus({
+ side: 'right',
+ collapsed: !this.rightPanelCollapsed,
+ });
},
},
};
</script>
<template>
-<div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': collapsed,
- }"
->
+<div class="multi-file-commit-panel-section">
<modal
v-if="showNewBranchModal"
:primary-button-label="__('Create new branch')"
@@ -108,28 +113,16 @@ export default {
@toggle="showNewBranchModal = false"
@submit="makeCommit(true)"
/>
- <button
- v-if="collapsed"
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn is-collapsed prepend-top-10 append-bottom-10"
- @click="toggleCollapsed"
- >
- <i
- aria-hidden="true"
- class="fa fa-angle-double-left"
- >
- </i>
- </button>
<commit-files-list
title="Staged"
:file-list="changedFiles"
- :collapsed="collapsed"
+ :collapsed="rightPanelCollapsed"
@toggleCollapsed="toggleCollapsed"
/>
<form
class="form-horizontal multi-file-commit-form"
@submit.prevent="tryCommit"
- v-if="!collapsed"
+ v-if="!rightPanelCollapsed"
>
<div class="multi-file-commit-fieldset">
<textarea
diff --git a/app/assets/javascripts/repo/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue
index 37bd9003e96..37bd9003e96 100644
--- a/app/assets/javascripts/repo/components/repo_edit_button.vue
+++ b/app/assets/javascripts/ide/components/repo_edit_button.vue
diff --git a/app/assets/javascripts/repo/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f37cbd1e961..221be4b9074 100644
--- a/app/assets/javascripts/repo/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -1,6 +1,6 @@
<script>
/* global monaco */
-import { mapGetters, mapActions } from 'vuex';
+import { mapState, mapGetters, mapActions } from 'vuex';
import flash from '../../flash';
import monacoLoader from '../monaco_loader';
import Editor from '../lib/editor';
@@ -24,6 +24,9 @@ export default {
...mapActions([
'getRawFileData',
'changeFileContent',
+ 'setFileLanguage',
+ 'setEditorPosition',
+ 'setFileEOL',
]),
initMonaco() {
if (this.shouldHideEditor) return;
@@ -43,12 +46,36 @@ export default {
const model = this.editor.createModel(this.activeFile);
this.editor.attachModel(model);
+
model.onChange((m) => {
this.changeFileContent({
file: this.activeFile,
content: m.getValue(),
});
});
+
+ // Handle Cursor Position
+ this.editor.onPositionChange((instance, e) => {
+ this.setEditorPosition({
+ editorRow: e.position.lineNumber,
+ editorColumn: e.position.column,
+ });
+ });
+
+ this.editor.setPosition({
+ lineNumber: this.activeFile.editorRow,
+ column: this.activeFile.editorColumn,
+ });
+
+ // Handle File Language
+ this.setFileLanguage({
+ fileLanguage: model.language,
+ });
+
+ // Get File eol
+ this.setFileEOL({
+ eol: model.eol,
+ });
},
},
watch: {
@@ -57,12 +84,22 @@ export default {
this.initMonaco();
}
},
+ leftPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
+ rightPanelCollapsed() {
+ this.editor.updateDimensions();
+ },
},
computed: {
...mapGetters([
'activeFile',
'activeFileExtension',
]),
+ ...mapState([
+ 'leftPanelCollapsed',
+ 'rightPanelCollapsed',
+ ]),
shouldHideEditor() {
return this.activeFile.binary && !this.activeFile.raw;
},
@@ -76,13 +113,14 @@ export default {
class="blob-viewer-container blob-editor-container"
>
<div
- v-show="shouldHideEditor"
+ v-if="shouldHideEditor"
v-html="activeFile.html"
>
</div>
<div
v-show="!shouldHideEditor"
ref="editor"
+ class="multi-file-editor-holder"
>
</div>
</div>
diff --git a/app/assets/javascripts/repo/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
index 75787ad6103..09ca11531b1 100644
--- a/app/assets/javascripts/repo/components/repo_file.vue
+++ b/app/assets/javascripts/ide/components/repo_file.vue
@@ -1,7 +1,8 @@
<script>
- import { mapActions, mapGetters } from 'vuex';
+ import { mapState } from 'vuex';
import timeAgoMixin from '../../vue_shared/mixins/timeago';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
+ import newDropdown from './new_dropdown/index.vue';
export default {
mixins: [
@@ -9,20 +10,22 @@
],
components: {
skeletonLoadingContainer,
+ newDropdown,
},
props: {
file: {
type: Object,
required: true,
},
+ showExtraColumns: {
+ type: Boolean,
+ default: false,
+ },
},
computed: {
- ...mapGetters([
- 'isCollapsed',
+ ...mapState([
+ 'leftPanelCollapsed',
]),
- isSubmodule() {
- return this.file.type === 'submodule';
- },
fileIcon() {
return {
'fa-spinner fa-spin': this.file.loading,
@@ -30,6 +33,12 @@
'fa-folder-open': !this.file.loading && this.file.opened,
};
},
+ isSubmodule() {
+ return this.file.type === 'submodule';
+ },
+ isTree() {
+ return this.file.type === 'tree';
+ },
levelIndentation() {
return {
marginLeft: `${this.file.level * 16}px`,
@@ -39,13 +48,39 @@
return this.file.id.substr(0, 8);
},
submoduleColSpan() {
- return !this.isCollapsed && this.isSubmodule ? 3 : 1;
+ return !this.leftPanelCollapsed && this.isSubmodule ? 3 : 1;
+ },
+ fileClass() {
+ if (this.file.type === 'blob') {
+ if (this.file.active) {
+ return 'file-open file-active';
+ }
+ return this.file.opened ? 'file-open' : '';
+ }
+ return '';
+ },
+ changedClass() {
+ return {
+ 'fa-circle unsaved-icon': this.file.changed || this.file.tempFile,
+ };
},
},
methods: {
- ...mapActions([
- 'clickedTreeRow',
- ]),
+ clickFile(row) {
+ // Manual Action if a tree is selected/opened
+ if (this.file.type === 'tree' && this.$router.currentRoute.path === `/project${row.url}`) {
+ this.$store.dispatch('toggleTreeOpen', {
+ endpoint: this.file.url,
+ tree: this.file,
+ });
+ }
+ this.$router.push(`/project${row.url}`);
+ },
+ },
+ updated() {
+ if (this.file.type === 'blob' && this.file.active) {
+ this.$el.scrollIntoView();
+ }
},
};
</script>
@@ -53,7 +88,8 @@
<template>
<tr
class="file"
- @click.prevent="clickedTreeRow(file)">
+ :class="fileClass"
+ @click="clickFile(file)">
<td
class="multi-file-table-name"
:colspan="submoduleColSpan"
@@ -66,11 +102,23 @@
>
</i>
<a
- :href="file.url"
class="repo-file-name"
>
{{ file.name }}
</a>
+ <new-dropdown
+ v-if="isTree"
+ :project-id="file.projectId"
+ :branch="file.branchId"
+ :path="file.path"
+ :parent="file"/>
+ <i
+ class="fa"
+ v-if="changedClass"
+ :class="changedClass"
+ aria-hidden="true"
+ >
+ </i>
<template v-if="isSubmodule && file.id">
@
<span class="commit-sha">
@@ -84,7 +132,7 @@
</template>
</td>
- <template v-if="!isCollapsed && !isSubmodule">
+ <template v-if="showExtraColumns && !isSubmodule">
<td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
<a
v-if="file.lastCommit.message"
diff --git a/app/assets/javascripts/repo/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
index 34f0d51819a..34f0d51819a 100644
--- a/app/assets/javascripts/repo/components/repo_file_buttons.vue
+++ b/app/assets/javascripts/ide/components/repo_file_buttons.vue
diff --git a/app/assets/javascripts/repo/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
index 8fa637d771f..7eb840c7608 100644
--- a/app/assets/javascripts/repo/components/repo_loading_file.vue
+++ b/app/assets/javascripts/ide/components/repo_loading_file.vue
@@ -1,5 +1,5 @@
<script>
- import { mapGetters } from 'vuex';
+ import { mapState } from 'vuex';
import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
export default {
@@ -7,8 +7,8 @@
skeletonLoadingContainer,
},
computed: {
- ...mapGetters([
- 'isCollapsed',
+ ...mapState([
+ 'leftPanelCollapsed',
]),
},
};
@@ -24,7 +24,7 @@
:small="true"
/>
</td>
- <template v-if="!isCollapsed">
+ <template v-if="!leftPanelCollapsed">
<td
class="hidden-sm hidden-xs">
<skeleton-loading-container
diff --git a/app/assets/javascripts/repo/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue
index a2b305bbd05..7cd359ea4ed 100644
--- a/app/assets/javascripts/repo/components/repo_prev_directory.vue
+++ b/app/assets/javascripts/ide/components/repo_prev_directory.vue
@@ -1,16 +1,14 @@
<script>
- import { mapGetters, mapState, mapActions } from 'vuex';
+ import { mapState, mapActions } from 'vuex';
export default {
computed: {
...mapState([
'parentTreeUrl',
- ]),
- ...mapGetters([
- 'isCollapsed',
+ 'leftPanelCollapsed',
]),
colSpanCondition() {
- return this.isCollapsed ? undefined : 3;
+ return this.leftPanelCollapsed ? undefined : 3;
},
},
methods: {
diff --git a/app/assets/javascripts/repo/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue
index 3d1e0297bd5..3d1e0297bd5 100644
--- a/app/assets/javascripts/repo/components/repo_preview.vue
+++ b/app/assets/javascripts/ide/components/repo_preview.vue
diff --git a/app/assets/javascripts/repo/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
index fb29a60df66..5bd63ac9ec5 100644
--- a/app/assets/javascripts/repo/components/repo_tab.vue
+++ b/app/assets/javascripts/ide/components/repo_tab.vue
@@ -27,16 +27,18 @@ export default {
methods: {
...mapActions([
- 'setFileActive',
'closeFile',
]),
+ clickFile(tab) {
+ this.$router.push(`/project${tab.url}`);
+ },
},
};
</script>
<template>
<li
- @click="setFileActive(tab)"
+ @click="clickFile(tab)"
>
<button
type="button"
diff --git a/app/assets/javascripts/repo/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
index ab0bef4f0ac..ab0bef4f0ac 100644
--- a/app/assets/javascripts/repo/components/repo_tabs.vue
+++ b/app/assets/javascripts/ide/components/repo_tabs.vue
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
new file mode 100644
index 00000000000..a9cbf8e370f
--- /dev/null
+++ b/app/assets/javascripts/ide/ide_router.js
@@ -0,0 +1,101 @@
+import Vue from 'vue';
+import VueRouter from 'vue-router';
+import store from './stores';
+import flash from '../flash';
+import {
+ getTreeEntry,
+} from './stores/utils';
+
+Vue.use(VueRouter);
+
+/**
+ * Routes below /-/ide/:
+
+/project/h5bp/html5-boilerplate/blob/master
+/project/h5bp/html5-boilerplate/blob/master/app/js/test.js
+
+/project/h5bp/html5-boilerplate/mr/123
+/project/h5bp/html5-boilerplate/mr/123/app/js/test.js
+
+/workspace/123
+/workspace/project/h5bp/html5-boilerplate/blob/my-special-branch
+/workspace/project/h5bp/html5-boilerplate/mr/123
+
+/ = /workspace
+
+/settings
+*/
+
+// Unfortunately Vue Router doesn't work without at least a fake component
+// If you do only data handling
+const EmptyRouterComponent = {
+ render(createElement) {
+ return createElement('div');
+ },
+};
+
+const router = new VueRouter({
+ mode: 'history',
+ base: `${gon.relative_url_root}/-/ide/`,
+ routes: [
+ {
+ path: '/project/:namespace/:project',
+ component: EmptyRouterComponent,
+ children: [
+ {
+ path: ':targetmode/:branch/*',
+ component: EmptyRouterComponent,
+ },
+ {
+ path: 'mr/:mrid',
+ component: EmptyRouterComponent,
+ },
+ ],
+ },
+ ],
+});
+
+router.beforeEach((to, from, next) => {
+ if (to.params.namespace && to.params.project) {
+ store.dispatch('getProjectData', {
+ namespace: to.params.namespace,
+ projectId: to.params.project,
+ })
+ .then(() => {
+ const fullProjectId = `${to.params.namespace}/${to.params.project}`;
+
+ if (to.params.branch) {
+ store.dispatch('getBranchData', {
+ projectId: fullProjectId,
+ branchId: to.params.branch,
+ });
+
+ store.dispatch('getTreeData', {
+ projectId: fullProjectId,
+ branch: to.params.branch,
+ endpoint: `/tree/${to.params.branch}`,
+ })
+ .then(() => {
+ if (to.params[0]) {
+ const treeEntry = getTreeEntry(store, `${to.params.namespace}/${to.params.project}/${to.params.branch}`, to.params[0]);
+ if (treeEntry) {
+ store.dispatch('handleTreeEntryAction', treeEntry);
+ }
+ }
+ })
+ .catch((e) => {
+ flash('Error while loading the branch files. Please try again.');
+ throw e;
+ });
+ }
+ })
+ .catch((e) => {
+ flash('Error while loading the project data. Please try again.');
+ throw e;
+ });
+ }
+
+ next();
+});
+
+export default router;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
new file mode 100644
index 00000000000..a96bd339f51
--- /dev/null
+++ b/app/assets/javascripts/ide/index.js
@@ -0,0 +1,55 @@
+import Vue from 'vue';
+import { mapActions } from 'vuex';
+import { convertPermissionToBoolean } from '../lib/utils/common_utils';
+import ide from './components/ide.vue';
+
+import store from './stores';
+import router from './ide_router';
+import Translate from '../vue_shared/translate';
+import ContextualSidebar from '../contextual_sidebar';
+
+function initIde(el) {
+ if (!el) return null;
+
+ return new Vue({
+ el,
+ store,
+ router,
+ components: {
+ ide,
+ },
+ methods: {
+ ...mapActions([
+ 'setInitialData',
+ ]),
+ },
+ created() {
+ const data = el.dataset;
+
+ this.setInitialData({
+ endpoints: {
+ rootEndpoint: data.url,
+ newMergeRequestUrl: data.newMergeRequestUrl,
+ rootUrl: data.rootUrl,
+ },
+ canCommit: convertPermissionToBoolean(data.canCommit),
+ onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
+ path: data.currentPath,
+ isRoot: convertPermissionToBoolean(data.root),
+ isInitialRoot: convertPermissionToBoolean(data.root),
+ });
+ },
+ render(createElement) {
+ return createElement('ide');
+ },
+ });
+}
+
+const ideElement = document.getElementById('ide');
+
+Vue.use(Translate);
+
+initIde(ideElement);
+
+const contextualSidebar = new ContextualSidebar();
+contextualSidebar.bindEvents();
diff --git a/app/assets/javascripts/repo/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js
index 84b29bdb600..84b29bdb600 100644
--- a/app/assets/javascripts/repo/lib/common/disposable.js
+++ b/app/assets/javascripts/ide/lib/common/disposable.js
diff --git a/app/assets/javascripts/repo/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
index 23c4811e6c0..14d9fe4771e 100644
--- a/app/assets/javascripts/repo/lib/common/model.js
+++ b/app/assets/javascripts/ide/lib/common/model.js
@@ -28,6 +28,14 @@ export default class Model {
return this.model.uri.toString();
}
+ get language() {
+ return this.model.getModeId();
+ }
+
+ get eol() {
+ return this.model.getEOL() === '\n' ? 'LF' : 'CRLF';
+ }
+
get path() {
return this.file.path;
}
diff --git a/app/assets/javascripts/repo/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
index fd462252795..fd462252795 100644
--- a/app/assets/javascripts/repo/lib/common/model_manager.js
+++ b/app/assets/javascripts/ide/lib/common/model_manager.js
diff --git a/app/assets/javascripts/repo/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
index 0954b7973c4..0954b7973c4 100644
--- a/app/assets/javascripts/repo/lib/decorations/controller.js
+++ b/app/assets/javascripts/ide/lib/decorations/controller.js
diff --git a/app/assets/javascripts/repo/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
index dc0b1c95e59..dc0b1c95e59 100644
--- a/app/assets/javascripts/repo/lib/diff/controller.js
+++ b/app/assets/javascripts/ide/lib/diff/controller.js
diff --git a/app/assets/javascripts/repo/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
index 0e37f5c4704..0e37f5c4704 100644
--- a/app/assets/javascripts/repo/lib/diff/diff.js
+++ b/app/assets/javascripts/ide/lib/diff/diff.js
diff --git a/app/assets/javascripts/repo/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
index e74c4046330..e74c4046330 100644
--- a/app/assets/javascripts/repo/lib/diff/diff_worker.js
+++ b/app/assets/javascripts/ide/lib/diff/diff_worker.js
diff --git a/app/assets/javascripts/repo/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
index db499444402..51e202b9348 100644
--- a/app/assets/javascripts/repo/lib/editor.js
+++ b/app/assets/javascripts/ide/lib/editor.js
@@ -22,6 +22,11 @@ export default class Editor {
this.modelManager = new ModelManager(this.monaco),
this.decorationsController = new DecorationsController(this),
);
+
+ this.debouncedUpdate = _.debounce(() => {
+ this.updateDimensions();
+ }, 200);
+ window.addEventListener('resize', this.debouncedUpdate, false);
}
createInstance(domElement) {
@@ -32,6 +37,9 @@ export default class Editor {
readOnly: false,
contextmenu: true,
scrollBeyondLastLine: false,
+ minimap: {
+ enabled: false,
+ },
}),
this.dirtyDiffController = new DirtyDiffController(
this.modelManager, this.decorationsController,
@@ -70,10 +78,32 @@ export default class Editor {
dispose() {
this.disposable.dispose();
+ window.removeEventListener('resize', this.debouncedUpdate);
// dispose main monaco instance
if (this.instance) {
this.instance = null;
}
}
+
+ updateDimensions() {
+ this.instance.layout();
+ }
+
+ setPosition({ lineNumber, column }) {
+ this.instance.revealPositionInCenter({
+ lineNumber,
+ column,
+ });
+ this.instance.setPosition({
+ lineNumber,
+ column,
+ });
+ }
+
+ onPositionChange(cb) {
+ this.disposable.add(
+ this.instance.onDidChangeCursorPosition(e => cb(this.instance, e)),
+ );
+ }
}
diff --git a/app/assets/javascripts/repo/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
index 701affc466e..701affc466e 100644
--- a/app/assets/javascripts/repo/lib/editor_options.js
+++ b/app/assets/javascripts/ide/lib/editor_options.js
diff --git a/app/assets/javascripts/repo/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
index af83a1ec0b4..af83a1ec0b4 100644
--- a/app/assets/javascripts/repo/monaco_loader.js
+++ b/app/assets/javascripts/ide/monaco_loader.js
diff --git a/app/assets/javascripts/repo/services/index.js b/app/assets/javascripts/ide/services/index.js
index 994d325e991..1fb24e93f2e 100644
--- a/app/assets/javascripts/repo/services/index.js
+++ b/app/assets/javascripts/ide/services/index.js
@@ -23,8 +23,11 @@ export default {
return Vue.http.get(file.rawPath, { params: { format: 'json' } })
.then(res => res.text());
},
- getBranchData(projectId, currentBranch) {
- return Api.branchSingle(projectId, currentBranch);
+ getProjectData(namespace, project) {
+ return Api.project(`${namespace}/${project}`);
+ },
+ getBranchData(projectId, currentBranchId) {
+ return Api.branchSingle(projectId, currentBranchId);
},
createBranch(projectId, payload) {
const url = Api.buildUrl(Api.createBranchPath).replace(':id', projectId);
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
new file mode 100644
index 00000000000..c01046c8c76
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions.js
@@ -0,0 +1,179 @@
+import Vue from 'vue';
+import { visitUrl } from '../../lib/utils/url_utility';
+import flash from '../../flash';
+import service from '../services';
+import * as types from './mutation_types';
+
+export const redirectToUrl = (_, url) => visitUrl(url);
+
+export const setInitialData = ({ commit }, data) =>
+ commit(types.SET_INITIAL_DATA, data);
+
+export const closeDiscardPopup = ({ commit }) =>
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+
+export const discardAllChanges = ({ commit, getters, dispatch }) => {
+ const changedFiles = getters.changedFiles;
+
+ changedFiles.forEach((file) => {
+ commit(types.DISCARD_FILE_CHANGES, file);
+
+ if (file.tempFile) {
+ dispatch('closeFile', { file, force: true });
+ }
+ });
+};
+
+export const closeAllFiles = ({ state, dispatch }) => {
+ state.openFiles.forEach(file => dispatch('closeFile', { file }));
+};
+
+export const toggleEditMode = (
+ { state, commit, getters, dispatch },
+ force = false,
+) => {
+ const changedFiles = getters.changedFiles;
+
+ if (changedFiles.length && !force) {
+ commit(types.TOGGLE_DISCARD_POPUP, true);
+ } else {
+ commit(types.TOGGLE_EDIT_MODE);
+ commit(types.TOGGLE_DISCARD_POPUP, false);
+ dispatch('toggleBlobView');
+
+ if (!state.editMode) {
+ dispatch('discardAllChanges');
+ }
+ }
+};
+
+export const toggleBlobView = ({ commit, state }) => {
+ if (state.editMode) {
+ commit(types.SET_EDIT_MODE);
+ } else {
+ commit(types.SET_PREVIEW_MODE);
+ }
+};
+
+export const setPanelCollapsedStatus = ({ commit }, { side, collapsed }) => {
+ if (side === 'left') {
+ commit(types.SET_LEFT_PANEL_COLLAPSED, collapsed);
+ } else {
+ commit(types.SET_RIGHT_PANEL_COLLAPSED, collapsed);
+ }
+};
+
+export const checkCommitStatus = ({ state }) =>
+ service
+ .getBranchData(state.currentProjectId, state.currentBranchId)
+ .then((data) => {
+ const { id } = data.commit;
+ const selectedBranch =
+ state.projects[state.currentProjectId].branches[state.currentBranchId];
+
+ if (selectedBranch.workingReference !== id) {
+ return true;
+ }
+
+ return false;
+ })
+ .catch(() => flash('Error checking branch data. Please try again.'));
+
+export const commitChanges = (
+ { commit, state, dispatch, getters },
+ { payload, newMr },
+) =>
+ service
+ .commit(state.currentProjectId, payload)
+ .then((data) => {
+ const { branch } = payload;
+ if (!data.short_id) {
+ flash(data.message);
+ return;
+ }
+
+ const selectedProject = state.projects[state.currentProjectId];
+ const lastCommit = {
+ commit_path: `${selectedProject.web_url}/commit/${data.id}`,
+ commit: {
+ message: data.message,
+ authored_date: data.committed_date,
+ },
+ };
+
+ flash(
+ `Your changes have been committed. Commit ${data.short_id} with ${
+ data.stats.additions
+ } additions, ${data.stats.deletions} deletions.`,
+ 'notice',
+ );
+
+ if (newMr) {
+ dispatch(
+ 'redirectToUrl',
+ `${
+ selectedProject.web_url
+ }/merge_requests/new?merge_request%5Bsource_branch%5D=${branch}`,
+ );
+ } else {
+ commit(types.SET_BRANCH_WORKING_REFERENCE, {
+ projectId: state.currentProjectId,
+ branchId: state.currentBranchId,
+ reference: data.id,
+ });
+
+ getters.changedFiles.forEach((entry) => {
+ commit(types.SET_LAST_COMMIT_DATA, {
+ entry,
+ lastCommit,
+ });
+ });
+
+ dispatch('discardAllChanges');
+ dispatch('closeAllFiles');
+
+ window.scrollTo(0, 0);
+ }
+ })
+ .catch(() => flash('Error committing changes. Please try again.'));
+
+export const createTempEntry = (
+ { state, dispatch },
+ { projectId, branchId, parent, name, type, content = '', base64 = false },
+) => {
+ const selectedParent = parent || state.trees[`${projectId}/${branchId}`];
+ if (type === 'tree') {
+ dispatch('createTempTree', {
+ projectId,
+ branchId,
+ parent: selectedParent,
+ name,
+ });
+ } else if (type === 'blob') {
+ dispatch('createTempFile', {
+ projectId,
+ branchId,
+ parent: selectedParent,
+ name,
+ base64,
+ content,
+ });
+ }
+};
+
+export const scrollToTab = () => {
+ Vue.nextTick(() => {
+ const tabs = document.getElementById('tabs');
+
+ if (tabs) {
+ const tabEl = tabs.querySelector('.active .repo-tab');
+
+ tabEl.focus();
+ }
+ });
+};
+
+export * from './actions/tree';
+export * from './actions/file';
+export * from './actions/project';
+export * from './actions/branch';
diff --git a/app/assets/javascripts/ide/stores/actions/branch.js b/app/assets/javascripts/ide/stores/actions/branch.js
new file mode 100644
index 00000000000..32bdf7fec22
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/branch.js
@@ -0,0 +1,43 @@
+import service from '../../services';
+import flash from '../../../flash';
+import * as types from '../mutation_types';
+
+export const getBranchData = (
+ { commit, state, dispatch },
+ { projectId, branchId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if ((typeof state.projects[`${projectId}`] === 'undefined' ||
+ !state.projects[`${projectId}`].branches[branchId])
+ || force) {
+ service.getBranchData(`${projectId}`, branchId)
+ .then((data) => {
+ const { id } = data.commit;
+ commit(types.SET_BRANCH, { projectPath: `${projectId}`, branchName: branchId, branch: data });
+ commit(types.SET_BRANCH_WORKING_REFERENCE, { projectId, branchId, reference: id });
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading branch data. Please try again.');
+ reject(new Error(`Branch not loaded - ${projectId}/${branchId}`));
+ });
+ } else {
+ resolve(state.projects[`${projectId}`].branches[branchId]);
+ }
+});
+
+export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
+ state.currentProjectId,
+ {
+ branch,
+ ref: state.currentBranchId,
+ },
+)
+.then(res => res.json())
+.then((data) => {
+ const branchName = data.name;
+ const url = location.href.replace(state.currentBranchId, branchName);
+
+ if (this.$router) this.$router.push(url);
+
+ commit(types.SET_CURRENT_BRANCH, branchName);
+});
diff --git a/app/assets/javascripts/repo/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index 5bae4fa826a..0f27d5bf1c3 100644
--- a/app/assets/javascripts/repo/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -2,9 +2,9 @@ import { normalizeHeaders } from '../../../lib/utils/common_utils';
import flash from '../../../flash';
import service from '../../services';
import * as types from '../mutation_types';
+import router from '../../ide_router';
import {
findEntry,
- pushState,
setPageTitle,
createTemp,
findIndexOfFile,
@@ -25,7 +25,7 @@ export const closeFile = ({ commit, state, dispatch }, { file, force = false })
dispatch('setFileActive', nextFileToOpen);
} else if (!state.openFiles.length) {
- pushState(file.parentTreeUrl);
+ router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
}
dispatch('getLastCommitData');
@@ -45,6 +45,9 @@ export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
// reset hash for line highlighting
location.hash = '';
+
+ commit(types.SET_CURRENT_PROJECT, file.projectId);
+ commit(types.SET_CURRENT_BRANCH, file.branchId);
};
export const getFileData = ({ state, commit, dispatch }, file) => {
@@ -63,8 +66,6 @@ export const getFileData = ({ state, commit, dispatch }, file) => {
commit(types.TOGGLE_FILE_OPEN, file);
dispatch('setFileActive', file);
commit(types.TOGGLE_LOADING, file);
-
- pushState(file.url);
})
.catch(() => {
commit(types.TOGGLE_LOADING, file);
@@ -82,21 +83,39 @@ export const changeFileContent = ({ commit }, { file, content }) => {
commit(types.UPDATE_FILE_CONTENT, { file, content });
};
-export const createTempFile = ({ state, commit, dispatch }, { tree, name, content = '', base64 = '' }) => {
+export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
+ commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
+};
+
+export const setFileEOL = ({ state, commit }, { eol }) => {
+ commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
+};
+
+export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
+ commit(types.SET_FILE_POSITION, { file: state.selectedFile, editorRow, editorColumn });
+};
+
+export const createTempFile = ({ state, commit, dispatch }, { projectId, branchId, parent, name, content = '', base64 = '' }) => {
+ const path = parent.path !== undefined ? parent.path : '';
+ // We need to do the replacement otherwise the web_url + file.url duplicate
+ const newUrl = `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${name}`;
const file = createTemp({
- name: name.replace(`${state.path}/`, ''),
- path: tree.path,
+ projectId,
+ branchId,
+ name: name.replace(`${path}/`, ''),
+ path,
type: 'blob',
- level: tree.level !== undefined ? tree.level + 1 : 0,
+ level: parent.level !== undefined ? parent.level + 1 : 0,
changed: true,
content,
base64,
+ url: newUrl,
});
- if (findEntry(tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
+ if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`);
commit(types.CREATE_TMP_FILE, {
- parent: tree,
+ parent,
file,
});
commit(types.TOGGLE_FILE_OPEN, file);
@@ -106,5 +125,7 @@ export const createTempFile = ({ state, commit, dispatch }, { tree, name, conten
dispatch('toggleEditMode', true);
}
+ router.push(`/project${file.url}`);
+
return Promise.resolve(file);
};
diff --git a/app/assets/javascripts/ide/stores/actions/project.js b/app/assets/javascripts/ide/stores/actions/project.js
new file mode 100644
index 00000000000..75e332090cb
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/project.js
@@ -0,0 +1,25 @@
+import service from '../../services';
+import flash from '../../../flash';
+import * as types from '../mutation_types';
+
+// eslint-disable-next-line import/prefer-default-export
+export const getProjectData = (
+ { commit, state, dispatch },
+ { namespace, projectId, force = false } = {},
+) => new Promise((resolve, reject) => {
+ if (!state.projects[`${namespace}/${projectId}`] || force) {
+ service.getProjectData(namespace, projectId)
+ .then(res => res.data)
+ .then((data) => {
+ commit(types.SET_PROJECT, { projectPath: `${namespace}/${projectId}`, project: data });
+ if (!state.currentProjectId) commit(types.SET_CURRENT_PROJECT, `${namespace}/${projectId}`);
+ resolve(data);
+ })
+ .catch(() => {
+ flash('Error loading project data. Please try again.');
+ reject(new Error(`Project not loaded ${namespace}/${projectId}`));
+ });
+ } else {
+ resolve(state.projects[`${namespace}/${projectId}`]);
+ }
+});
diff --git a/app/assets/javascripts/ide/stores/actions/tree.js b/app/assets/javascripts/ide/stores/actions/tree.js
new file mode 100644
index 00000000000..25909400a75
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/actions/tree.js
@@ -0,0 +1,188 @@
+import { visitUrl } from '../../../lib/utils/url_utility';
+import { normalizeHeaders } from '../../../lib/utils/common_utils';
+import flash from '../../../flash';
+import service from '../../services';
+import * as types from '../mutation_types';
+import router from '../../ide_router';
+import {
+ setPageTitle,
+ findEntry,
+ createTemp,
+ createOrMergeEntry,
+} from '../utils';
+
+export const getTreeData = (
+ { commit, state, dispatch },
+ { endpoint, tree = null, projectId, branch, force = false } = {},
+) => new Promise((resolve, reject) => {
+ // We already have the base tree so we resolve immediately
+ if (!tree && state.trees[`${projectId}/${branch}`] && !force) {
+ resolve();
+ } else {
+ if (tree) commit(types.TOGGLE_LOADING, tree);
+ const selectedProject = state.projects[projectId];
+ // We are merging the web_url that we got on the project info with the endpoint
+ // we got on the tree entry, as both contain the projectId, we replace it in the tree endpoint
+ const completeEndpoint = selectedProject.web_url + (endpoint).replace(projectId, '');
+ if (completeEndpoint && (!tree || !tree.tempFile)) {
+ service.getTreeData(completeEndpoint)
+ .then((res) => {
+ const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
+
+ setPageTitle(pageTitle);
+
+ return res.json();
+ })
+ .then((data) => {
+ if (!state.isInitialRoot) {
+ commit(types.SET_ROOT, data.path === '/');
+ }
+
+ dispatch('updateDirectoryData', { data, tree, projectId, branch });
+ const selectedTree = tree || state.trees[`${projectId}/${branch}`];
+
+ commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
+ commit(types.SET_LAST_COMMIT_URL, { tree: selectedTree, url: data.last_commit_path });
+ if (tree) commit(types.TOGGLE_LOADING, selectedTree);
+
+ const prevLastCommitPath = selectedTree.lastCommitPath;
+ if (prevLastCommitPath !== null) {
+ dispatch('getLastCommitData', selectedTree);
+ }
+ resolve(data);
+ })
+ .catch((e) => {
+ flash('Error loading tree data. Please try again.');
+ if (tree) commit(types.TOGGLE_LOADING, tree);
+ reject(e);
+ });
+ } else {
+ resolve();
+ }
+ }
+});
+
+export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
+ if (tree.opened) {
+ // send empty data to clear the tree
+ const data = { trees: [], blobs: [], submodules: [] };
+
+ dispatch('updateDirectoryData', { data, tree, projectId: tree.projectId, branchId: tree.branchId });
+ } else {
+ dispatch('getTreeData', { endpoint, tree, projectId: tree.projectId, branch: tree.branchId });
+ }
+
+ commit(types.TOGGLE_TREE_OPEN, tree);
+};
+
+export const handleTreeEntryAction = ({ commit, dispatch }, row) => {
+ if (row.type === 'tree') {
+ dispatch('toggleTreeOpen', {
+ endpoint: row.url,
+ tree: row,
+ });
+ } else if (row.type === 'submodule') {
+ commit(types.TOGGLE_LOADING, row);
+ visitUrl(row.url);
+ } else if (row.type === 'blob' && row.opened) {
+ dispatch('setFileActive', row);
+ } else {
+ dispatch('getFileData', row);
+ }
+};
+
+export const createTempTree = (
+ { state, commit, dispatch },
+ { projectId, branchId, parent, name },
+) => {
+ let selectedTree = parent;
+ const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
+
+ dirNames.forEach((dirName) => {
+ const foundEntry = findEntry(selectedTree.tree, 'tree', dirName);
+
+ if (!foundEntry) {
+ const path = selectedTree.path !== undefined ? selectedTree.path : '';
+ const tmpEntry = createTemp({
+ projectId,
+ branchId,
+ name: dirName,
+ path,
+ type: 'tree',
+ level: selectedTree.level !== undefined ? selectedTree.level + 1 : 0,
+ tree: [],
+ url: `/${projectId}/blob/${branchId}/${path}${path ? '/' : ''}${dirName}`,
+ });
+
+ commit(types.CREATE_TMP_TREE, {
+ parent: selectedTree,
+ tmpEntry,
+ });
+ commit(types.TOGGLE_TREE_OPEN, tmpEntry);
+
+ router.push(`/project${tmpEntry.url}`);
+
+ selectedTree = tmpEntry;
+ } else {
+ selectedTree = foundEntry;
+ }
+ });
+};
+
+export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
+ if (!tree || tree.lastCommitPath === null || !tree.lastCommitPath) return;
+
+ service.getTreeLastCommit(tree.lastCommitPath)
+ .then((res) => {
+ const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
+
+ commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
+
+ return res.json();
+ })
+ .then((data) => {
+ data.forEach((lastCommit) => {
+ const entry = findEntry(tree.tree, lastCommit.type, lastCommit.file_name);
+
+ if (entry) {
+ commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
+ }
+ });
+
+ dispatch('getLastCommitData', tree);
+ })
+ .catch(() => flash('Error fetching log data.'));
+};
+
+export const updateDirectoryData = (
+ { commit, state },
+ { data, tree, projectId, branch },
+) => {
+ if (!tree) {
+ const existingTree = state.trees[`${projectId}/${branch}`];
+ if (!existingTree) {
+ commit(types.CREATE_TREE, { treePath: `${projectId}/${branch}` });
+ }
+ }
+
+ const selectedTree = tree || state.trees[`${projectId}/${branch}`];
+ const level = selectedTree.level !== undefined ? selectedTree.level + 1 : 0;
+ const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
+ const createEntry = (entry, type) => createOrMergeEntry({
+ tree: selectedTree,
+ projectId: `${projectId}`,
+ branchId: branch,
+ entry,
+ level,
+ type,
+ parentTreeUrl,
+ });
+
+ const formattedData = [
+ ...data.trees.map(t => createEntry(t, 'tree')),
+ ...data.submodules.map(m => createEntry(m, 'submodule')),
+ ...data.blobs.map(b => createEntry(b, 'blob')),
+ ];
+
+ commit(types.SET_DIRECTORY_DATA, { tree: selectedTree, data: formattedData });
+};
diff --git a/app/assets/javascripts/ide/stores/getters.js b/app/assets/javascripts/ide/stores/getters.js
new file mode 100644
index 00000000000..6b51ccff817
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/getters.js
@@ -0,0 +1,19 @@
+export const changedFiles = state => state.openFiles.filter(file => file.changed);
+
+export const activeFile = state => state.openFiles.find(file => file.active) || null;
+
+export const activeFileExtension = (state) => {
+ const file = activeFile(state);
+ return file ? `.${file.path.split('.').pop()}` : '';
+};
+
+export const canEditFile = (state) => {
+ const currentActiveFile = activeFile(state);
+
+ return state.canCommit &&
+ (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
+};
+
+export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
+
+export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/index.js b/app/assets/javascripts/ide/stores/index.js
index 6ac9bfd8189..6ac9bfd8189 100644
--- a/app/assets/javascripts/repo/stores/index.js
+++ b/app/assets/javascripts/ide/stores/index.js
diff --git a/app/assets/javascripts/repo/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
index bc3390f1506..4e3c10972ba 100644
--- a/app/assets/javascripts/repo/stores/mutation_types.js
+++ b/app/assets/javascripts/ide/stores/mutation_types.js
@@ -1,16 +1,27 @@
export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_COMMIT_REF = 'SET_COMMIT_REF';
export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
export const SET_ROOT = 'SET_ROOT';
-export const SET_PREVIOUS_URL = 'SET_PREVIOUS_URL';
export const SET_LAST_COMMIT_DATA = 'SET_LAST_COMMIT_DATA';
+export const SET_LEFT_PANEL_COLLAPSED = 'SET_LEFT_PANEL_COLLAPSED';
+export const SET_RIGHT_PANEL_COLLAPSED = 'SET_RIGHT_PANEL_COLLAPSED';
+
+// Project Mutation Types
+export const SET_PROJECT = 'SET_PROJECT';
+export const SET_CURRENT_PROJECT = 'SET_CURRENT_PROJECT';
+export const TOGGLE_PROJECT_OPEN = 'TOGGLE_PROJECT_OPEN';
+
+// Branch Mutation Types
+export const SET_BRANCH = 'SET_BRANCH';
+export const SET_BRANCH_WORKING_REFERENCE = 'SET_BRANCH_WORKING_REFERENCE';
+export const TOGGLE_BRANCH_OPEN = 'TOGGLE_BRANCH_OPEN';
// Tree mutation types
export const SET_DIRECTORY_DATA = 'SET_DIRECTORY_DATA';
export const TOGGLE_TREE_OPEN = 'TOGGLE_TREE_OPEN';
export const CREATE_TMP_TREE = 'CREATE_TMP_TREE';
export const SET_LAST_COMMIT_URL = 'SET_LAST_COMMIT_URL';
+export const CREATE_TREE = 'CREATE_TREE';
// File mutation types
export const SET_FILE_DATA = 'SET_FILE_DATA';
@@ -18,6 +29,9 @@ export const TOGGLE_FILE_OPEN = 'TOGGLE_FILE_OPEN';
export const SET_FILE_ACTIVE = 'SET_FILE_ACTIVE';
export const SET_FILE_RAW_DATA = 'SET_FILE_RAW_DATA';
export const UPDATE_FILE_CONTENT = 'UPDATE_FILE_CONTENT';
+export const SET_FILE_LANGUAGE = 'SET_FILE_LANGUAGE';
+export const SET_FILE_POSITION = 'SET_FILE_POSITION';
+export const SET_FILE_EOL = 'SET_FILE_EOL';
export const DISCARD_FILE_CHANGES = 'DISCARD_FILE_CHANGES';
export const CREATE_TMP_FILE = 'CREATE_TMP_FILE';
@@ -28,3 +42,4 @@ export const TOGGLE_EDIT_MODE = 'TOGGLE_EDIT_MODE';
export const TOGGLE_DISCARD_POPUP = 'TOGGLE_DISCARD_POPUP';
export const SET_CURRENT_BRANCH = 'SET_CURRENT_BRANCH';
+
diff --git a/app/assets/javascripts/repo/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index ae2ba5bedf7..2fed9019cb6 100644
--- a/app/assets/javascripts/repo/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -1,4 +1,5 @@
import * as types from './mutation_types';
+import projectMutations from './mutations/project';
import fileMutations from './mutations/file';
import treeMutations from './mutations/tree';
import branchMutations from './mutations/branch';
@@ -32,29 +33,32 @@ export default {
discardPopupOpen,
});
},
- [types.SET_COMMIT_REF](state, ref) {
- Object.assign(state, {
- currentRef: ref,
- });
- },
[types.SET_ROOT](state, isRoot) {
Object.assign(state, {
isRoot,
isInitialRoot: isRoot,
});
},
- [types.SET_PREVIOUS_URL](state, previousUrl) {
+ [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
+ Object.assign(state, {
+ leftPanelCollapsed: collapsed,
+ });
+ },
+ [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
Object.assign(state, {
- previousUrl,
+ rightPanelCollapsed: collapsed,
});
},
[types.SET_LAST_COMMIT_DATA](state, { entry, lastCommit }) {
Object.assign(entry.lastCommit, {
+ id: lastCommit.commit.id,
url: lastCommit.commit_path,
message: lastCommit.commit.message,
+ author: lastCommit.commit.author_name,
updatedAt: lastCommit.commit.authored_date,
});
},
+ ...projectMutations,
...fileMutations,
...treeMutations,
...branchMutations,
diff --git a/app/assets/javascripts/ide/stores/mutations/branch.js b/app/assets/javascripts/ide/stores/mutations/branch.js
new file mode 100644
index 00000000000..04b9582c5bb
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/branch.js
@@ -0,0 +1,28 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_BRANCH](state, currentBranchId) {
+ Object.assign(state, {
+ currentBranchId,
+ });
+ },
+ [types.SET_BRANCH](state, { projectPath, branchName, branch }) {
+ // Add client side properties
+ Object.assign(branch, {
+ treeId: `${projectPath}/${branchName}`,
+ active: true,
+ workingReference: '',
+ });
+
+ Object.assign(state.projects[projectPath], {
+ branches: {
+ [branchName]: branch,
+ },
+ });
+ },
+ [types.SET_BRANCH_WORKING_REFERENCE](state, { projectId, branchId, reference }) {
+ Object.assign(state.projects[projectId].branches[branchId], {
+ workingReference: reference,
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
index f9ba80b9dc2..5f3655b0092 100644
--- a/app/assets/javascripts/repo/stores/mutations/file.js
+++ b/app/assets/javascripts/ide/stores/mutations/file.js
@@ -6,6 +6,10 @@ export default {
Object.assign(file, {
active,
});
+
+ Object.assign(state, {
+ selectedFile: file,
+ });
},
[types.TOGGLE_FILE_OPEN](state, file) {
Object.assign(file, {
@@ -42,6 +46,22 @@ export default {
changed,
});
},
+ [types.SET_FILE_LANGUAGE](state, { file, fileLanguage }) {
+ Object.assign(file, {
+ fileLanguage,
+ });
+ },
+ [types.SET_FILE_EOL](state, { file, eol }) {
+ Object.assign(file, {
+ eol,
+ });
+ },
+ [types.SET_FILE_POSITION](state, { file, editorRow, editorColumn }) {
+ Object.assign(file, {
+ editorRow,
+ editorColumn,
+ });
+ },
[types.DISCARD_FILE_CHANGES](state, file) {
Object.assign(file, {
content: '',
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
new file mode 100644
index 00000000000..2816562a919
--- /dev/null
+++ b/app/assets/javascripts/ide/stores/mutations/project.js
@@ -0,0 +1,23 @@
+import * as types from '../mutation_types';
+
+export default {
+ [types.SET_CURRENT_PROJECT](state, currentProjectId) {
+ Object.assign(state, {
+ currentProjectId,
+ });
+ },
+ [types.SET_PROJECT](state, { projectPath, project }) {
+ // Add client side properties
+ Object.assign(project, {
+ tree: [],
+ branches: {},
+ active: true,
+ });
+
+ Object.assign(state, {
+ projects: Object.assign({}, state.projects, {
+ [projectPath]: project,
+ }),
+ });
+ },
+};
diff --git a/app/assets/javascripts/repo/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
index 130221c9fda..4fe438ab465 100644
--- a/app/assets/javascripts/repo/stores/mutations/tree.js
+++ b/app/assets/javascripts/ide/stores/mutations/tree.js
@@ -6,6 +6,15 @@ export default {
opened: !tree.opened,
});
},
+ [types.CREATE_TREE](state, { treePath }) {
+ Object.assign(state, {
+ trees: Object.assign({}, state.trees, {
+ [treePath]: {
+ tree: [],
+ },
+ }),
+ });
+ },
[types.SET_DIRECTORY_DATA](state, { data, tree }) {
Object.assign(tree, {
tree: data,
diff --git a/app/assets/javascripts/repo/stores/state.js b/app/assets/javascripts/ide/stores/state.js
index 0068834831e..539e382830f 100644
--- a/app/assets/javascripts/repo/stores/state.js
+++ b/app/assets/javascripts/ide/stores/state.js
@@ -1,10 +1,10 @@
export default () => ({
canCommit: false,
- currentBranch: '',
- currentBlobView: 'repo-preview',
- currentRef: '',
+ currentProjectId: '',
+ currentBranchId: '',
+ currentBlobView: 'repo-editor',
discardPopupOpen: false,
- editMode: false,
+ editMode: true,
endpoints: {},
isRoot: false,
isInitialRoot: false,
@@ -12,13 +12,11 @@ export default () => ({
loading: false,
onTopOfBranch: false,
openFiles: [],
+ selectedFile: null,
path: '',
- project: {
- id: 0,
- name: '',
- url: '',
- },
parentTreeUrl: '',
- previousUrl: '',
- tree: [],
+ trees: {},
+ projects: {},
+ leftPanelCollapsed: false,
+ rightPanelCollapsed: true,
});
diff --git a/app/assets/javascripts/repo/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
index fae1f4439a9..29e3ab5d040 100644
--- a/app/assets/javascripts/repo/stores/utils.js
+++ b/app/assets/javascripts/ide/stores/utils.js
@@ -2,6 +2,8 @@ export const dataStructure = () => ({
id: '',
key: '',
type: '',
+ projectId: '',
+ branchId: '',
name: '',
url: '',
path: '',
@@ -15,9 +17,11 @@ export const dataStructure = () => ({
changed: false,
lastCommitPath: '',
lastCommit: {
+ id: '',
url: '',
message: '',
updatedAt: '',
+ author: '',
},
tree_url: '',
blamePath: '',
@@ -31,11 +35,17 @@ export const dataStructure = () => ({
parentTreeUrl: '',
renderError: false,
base64: false,
+ editorRow: 1,
+ editorColumn: 1,
+ fileLanguage: '',
+ eol: '',
});
export const decorateData = (entity) => {
const {
id,
+ projectId,
+ branchId,
type,
url,
name,
@@ -56,6 +66,8 @@ export const decorateData = (entity) => {
return {
...dataStructure(),
id,
+ projectId,
+ branchId,
key: `${name}-${type}-${id}`,
type,
name,
@@ -75,24 +87,51 @@ export const decorateData = (entity) => {
};
};
-export const findEntry = (state, type, name) => state.tree.find(
+/*
+ Takes the multi-dimensional tree and returns a flattened array.
+ This allows for the table to recursively render the table rows but keeps the data
+ structure nested to make it easier to add new files/directories.
+*/
+export const treeList = (state, treeId) => {
+ const baseTree = state.trees[treeId];
+ if (baseTree) {
+ const mapTree = arr => (!arr.tree || !arr.tree.length ?
+ [] : _.map(arr.tree, a => [a, mapTree(a)]));
+
+ return _.chain(baseTree.tree)
+ .map(arr => [arr, mapTree(arr)])
+ .flatten()
+ .value();
+ }
+ return [];
+};
+
+export const getTree = state => (namespace, projectId, branch) => state.trees[`${namespace}/${projectId}/${branch}`];
+
+export const getTreeEntry = (store, treeId, path) => {
+ const fileList = treeList(store.state, treeId);
+ return fileList ? fileList.find(file => file.path === path) : null;
+};
+
+export const findEntry = (tree, type, name) => tree.find(
f => f.type === type && f.name === name,
);
+
export const findIndexOfFile = (state, file) => state.findIndex(f => f.path === file.path);
export const setPageTitle = (title) => {
document.title = title;
};
-export const pushState = (url) => {
- history.pushState({ url }, '', url);
-};
-
-export const createTemp = ({ name, path, type, level, changed, content, base64 }) => {
+export const createTemp = ({
+ projectId, branchId, name, path, type, level, changed, content, base64, url,
+}) => {
const treePath = path ? `${path}/${name}` : name;
return decorateData({
id: new Date().getTime().toString(),
+ projectId,
+ branchId,
name,
type,
tempFile: true,
@@ -104,11 +143,18 @@ export const createTemp = ({ name, path, type, level, changed, content, base64 }
level,
base64,
renderError: base64,
+ url,
});
};
-export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level }) => {
- const found = findEntry(tree, type, entry.name);
+export const createOrMergeEntry = ({ tree,
+ projectId,
+ branchId,
+ entry,
+ type,
+ parentTreeUrl,
+ level }) => {
+ const found = findEntry(tree.tree || tree, type, entry.name);
if (found) {
return Object.assign({}, found, {
@@ -120,6 +166,8 @@ export const createOrMergeEntry = ({ tree, entry, type, parentTreeUrl, level })
return decorateData({
...entry,
+ projectId,
+ branchId,
type,
parentTreeUrl,
level,
diff --git a/app/assets/javascripts/issue_show/components/app.vue b/app/assets/javascripts/issue_show/components/app.vue
index 25ebe5314e0..952f49d522e 100644
--- a/app/assets/javascripts/issue_show/components/app.vue
+++ b/app/assets/javascripts/issue_show/components/app.vue
@@ -32,7 +32,7 @@ export default {
showInlineEditButton: {
type: Boolean,
required: false,
- default: false,
+ default: true,
},
showDeleteButton: {
type: Boolean,
diff --git a/app/assets/javascripts/issue_show/components/title.vue b/app/assets/javascripts/issue_show/components/title.vue
index a363d06d950..b7e6eadd440 100644
--- a/app/assets/javascripts/issue_show/components/title.vue
+++ b/app/assets/javascripts/issue_show/components/title.vue
@@ -79,7 +79,7 @@
v-tooltip
v-if="showInlineEditButton && canUpdate"
type="button"
- class="btn btn-default btn-edit btn-svg"
+ class="btn btn-default btn-edit btn-svg js-issuable-edit"
v-html="pencilIcon"
title="Edit title and description"
data-placement="bottom"
diff --git a/app/assets/javascripts/issue_show/index.js b/app/assets/javascripts/issue_show/index.js
index 7b762496ba5..75dfdedcf1b 100644
--- a/app/assets/javascripts/issue_show/index.js
+++ b/app/assets/javascripts/issue_show/index.js
@@ -1,5 +1,4 @@
import Vue from 'vue';
-import eventHub from './event_hub';
import issuableApp from './components/app.vue';
import '../vue_shared/vue_resource_interceptor';
@@ -7,12 +6,6 @@ document.addEventListener('DOMContentLoaded', () => {
const initialDataEl = document.getElementById('js-issuable-app-initial-data');
const props = JSON.parse(initialDataEl.innerHTML.replace(/&quot;/g, '"'));
- $('.js-issuable-edit').on('click', (e) => {
- e.preventDefault();
-
- eventHub.$emit('open.form');
- });
-
return new Vue({
el: document.getElementById('js-issuable-app'),
components: {
diff --git a/app/assets/javascripts/layout_nav.js b/app/assets/javascripts/layout_nav.js
index a6f82b247e2..ab3cc29146a 100644
--- a/app/assets/javascripts/layout_nav.js
+++ b/app/assets/javascripts/layout_nav.js
@@ -1,59 +1,51 @@
-/* eslint-disable func-names, space-before-function-paren, no-var, prefer-arrow-callback, no-unused-vars, one-var, one-var-declaration-per-line, vars-on-top, max-len */
-import _ from 'underscore';
-import Cookies from 'js-cookie';
import ContextualSidebar from './contextual_sidebar';
import initFlyOutNav from './fly_out_nav';
-(function() {
- var hideEndFade;
+function hideEndFade($scrollingTabs) {
+ $scrollingTabs.each(function scrollTabsLoop() {
+ const $this = $(this);
+ $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
+ });
+}
- hideEndFade = function($scrollingTabs) {
- return $scrollingTabs.each(function() {
- var $this;
- $this = $(this);
- return $this.siblings('.fade-right').toggleClass('scrolling', $this.width() < $this.prop('scrollWidth'));
- });
- };
+export default function initLayoutNav() {
+ const contextualSidebar = new ContextualSidebar();
+ contextualSidebar.bindEvents();
+
+ initFlyOutNav();
$(document).on('init.scrolling-tabs', () => {
const $scrollingTabs = $('.scrolling-tabs').not('.is-initialized');
$scrollingTabs.addClass('is-initialized');
- hideEndFade($scrollingTabs);
- $(window).off('resize.nav').on('resize.nav', function() {
- return hideEndFade($scrollingTabs);
- });
- $scrollingTabs.off('scroll').on('scroll', function(event) {
- var $this, currentPosition, maxPosition;
- $this = $(this);
- currentPosition = $this.scrollLeft();
- maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+ $(window).on('resize.nav', () => {
+ hideEndFade($scrollingTabs);
+ }).trigger('resize.nav');
+
+ $scrollingTabs.on('scroll', function tabsScrollEvent() {
+ const $this = $(this);
+ const currentPosition = $this.scrollLeft();
+ const maxPosition = $this.prop('scrollWidth') - $this.outerWidth();
+
$this.siblings('.fade-left').toggleClass('scrolling', currentPosition > 0);
- return $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
+ $this.siblings('.fade-right').toggleClass('scrolling', currentPosition < maxPosition - 1);
});
- $scrollingTabs.each(function () {
- var $this = $(this);
- var scrollingTabWidth = $this.width();
- var $active = $this.find('.active');
- var activeWidth = $active.width();
+ $scrollingTabs.each(function scrollTabsEachLoop() {
+ const $this = $(this);
+ const scrollingTabWidth = $this.width();
+ const $active = $this.find('.active');
+ const activeWidth = $active.width();
if ($active.length) {
- var offset = $active.offset().left + activeWidth;
+ const offset = $active.offset().left + activeWidth;
if (offset > scrollingTabWidth - 30) {
- var scrollLeft = scrollingTabWidth / 2;
- scrollLeft = (offset - scrollLeft) - (activeWidth / 2);
+ const scrollLeft = (offset - (scrollingTabWidth / 2)) - (activeWidth / 2);
+
$this.scrollLeft(scrollLeft);
}
}
});
- });
-
- $(() => {
- const contextualSidebar = new ContextualSidebar();
- contextualSidebar.bindEvents();
-
- initFlyOutNav();
- });
-}).call(window);
+ }).trigger('init.scrolling-tabs');
+}
diff --git a/app/assets/javascripts/lib/utils/datetime_utility.js b/app/assets/javascripts/lib/utils/datetime_utility.js
index 198b5164c92..1fa6715180e 100644
--- a/app/assets/javascripts/lib/utils/datetime_utility.js
+++ b/app/assets/javascripts/lib/utils/datetime_utility.js
@@ -2,7 +2,7 @@ import timeago from 'timeago.js';
import dateFormat from 'vendor/date.format';
import { pluralize } from './text_utility';
import {
- lang,
+ languageCode,
s__,
} from '../../locale';
@@ -24,7 +24,15 @@ export const getDayName = date => ['Sunday', 'Monday', 'Tuesday', 'Wednesday', '
*/
export const formatDate = datetime => dateFormat(datetime, 'mmm d, yyyy h:MMtt Z');
+/**
+ * Timeago uses underscores instead of dashes to separate language from country code.
+ *
+ * see https://github.com/hustcc/timeago.js/tree/v3.0.0/locales
+ */
+const timeagoLanguageCode = languageCode().replace(/-/g, '_');
+
let timeagoInstance;
+
/**
* Sets a timeago Instance
*/
@@ -67,8 +75,8 @@ export function getTimeago() {
][index];
};
- timeago.register(lang, locale);
- timeago.register(`${lang}-remaining`, localeRemaining);
+ timeago.register(timeagoLanguageCode, locale);
+ timeago.register(`${timeagoLanguageCode}-remaining`, localeRemaining);
timeagoInstance = timeago();
}
@@ -83,7 +91,7 @@ export const renderTimeago = ($els) => {
const timeagoEls = $els || document.querySelectorAll('.js-timeago-render');
// timeago.js sets timeouts internally for each timeago value to be updated in real time
- getTimeago().render(timeagoEls, lang);
+ getTimeago().render(timeagoEls, timeagoLanguageCode);
};
/**
@@ -118,7 +126,7 @@ export const timeFor = (time, expiredLabel) => {
if (new Date(time) < new Date()) {
return expiredLabel || s__('Timeago|Past due');
}
- return getTimeago().format(time, `${lang}-remaining`).trim();
+ return getTimeago().format(time, `${timeagoLanguageCode}-remaining`).trim();
};
export const getDayDifference = (a, b) => {
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 15a3a91c5f5..59bfa482bb0 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -41,7 +41,7 @@ import Flash, { removeFlashClickListener } from './flash';
import './gl_dropdown';
import initTodoToggle from './header';
import initImporterStatus from './importer_status';
-import './layout_nav';
+import initLayoutNav from './layout_nav';
import LazyLoader from './lazy_loader';
import './line_highlighter';
import initLogoAnimation from './logo';
@@ -89,6 +89,7 @@ $(function () {
var fitSidebarForSize;
initBreadcrumbs();
+ initLayoutNav();
initImporterStatus();
initTodoToggle();
initLogoAnimation();
@@ -261,8 +262,6 @@ $(function () {
renderTimeago();
- $(document).trigger('init.scrolling-tabs');
-
$('form.filter-form').on('submit', function (event) {
const link = document.createElement('a');
link.href = this.action;
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index cdae287658b..eede04a06cd 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -1,5 +1,8 @@
<script>
- import d3 from 'd3';
+ import { scaleLinear, scaleTime } from 'd3-scale';
+ import { axisLeft, axisBottom } from 'd3-axis';
+ import { max, extent } from 'd3-array';
+ import { select } from 'd3-selection';
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
@@ -7,10 +10,12 @@
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
- import { timeScaleFormat, bisectDate } from '../utils/date_time_formatters';
+ import { bisectDate, timeScaleFormat } from '../utils/date_time_formatters';
import createTimeSeries from '../utils/multiple_time_series';
import bp from '../../breakpoints';
+ const d3 = { scaleLinear, scaleTime, axisLeft, axisBottom, max, extent, select };
+
export default {
props: {
graphData: {
@@ -156,25 +161,22 @@
this.baseGraphHeight = this.baseGraphHeight += (this.timeSeries.length - 3) * 20;
}
- const axisXScale = d3.time.scale()
+ const axisXScale = d3.scaleTime()
.range([0, this.graphWidth - 70]);
- const axisYScale = d3.scale.linear()
+ const axisYScale = d3.scaleLinear()
.range([this.graphHeight - this.graphHeightOffset, 0]);
const allValues = this.timeSeries.reduce((all, { values }) => all.concat(values), []);
axisXScale.domain(d3.extent(allValues, d => d.time));
axisYScale.domain([0, d3.max(allValues.map(d => d.value))]);
- const xAxis = d3.svg.axis()
+ const xAxis = d3.axisBottom()
.scale(axisXScale)
- .ticks(d3.time.minute, 60)
- .tickFormat(timeScaleFormat)
- .orient('bottom');
+ .tickFormat(timeScaleFormat);
- const yAxis = d3.svg.axis()
+ const yAxis = d3.axisLeft()
.scale(axisYScale)
- .ticks(measurements.yTicks)
- .orient('left');
+ .ticks(measurements.yTicks);
d3.select(this.$refs.baseSvg).select('.x-axis').call(xAxis);
diff --git a/app/assets/javascripts/monitoring/utils/date_time_formatters.js b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
index ad07a8465e2..48bdec1e030 100644
--- a/app/assets/javascripts/monitoring/utils/date_time_formatters.js
+++ b/app/assets/javascripts/monitoring/utils/date_time_formatters.js
@@ -1,17 +1,32 @@
-import d3 from 'd3';
+import { timeFormat as time } from 'd3-time-format';
+import { timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear } from 'd3-time';
+import { bisector } from 'd3-array';
-export const dateFormat = d3.time.format('%b %-d, %Y');
-export const dateFormatWithName = d3.time.format('%a, %b %-d');
-export const timeFormat = d3.time.format('%-I:%M%p');
+const d3 = { time, bisector, timeSecond, timeMinute, timeHour, timeDay, timeMonth, timeYear };
+
+export const dateFormat = d3.time('%b %-d, %Y');
+export const timeFormat = d3.time('%-I:%M%p');
+export const dateFormatWithName = d3.time('%a, %b %-d');
export const bisectDate = d3.bisector(d => d.time).left;
-export const timeScaleFormat = d3.time.format.multi([
- ['.%L', d => d.getMilliseconds()],
- [':%S', d => d.getSeconds()],
- ['%-I:%M', d => d.getMinutes()],
- ['%-I %p', d => d.getHours()],
- ['%a %-d', d => d.getDay() && d.getDate() !== 1],
- ['%b %-d', d => d.getDate() !== 1],
- ['%B', d => d.getMonth()],
- ['%Y', () => true],
-]);
+export function timeScaleFormat(date) {
+ let formatFunction;
+ if (d3.timeSecond(date) < date) {
+ formatFunction = d3.time('.%L');
+ } else if (d3.timeMinute(date) < date) {
+ formatFunction = d3.time(':%S');
+ } else if (d3.timeHour(date) < date) {
+ formatFunction = d3.time('%-I:%M');
+ } else if (d3.timeDay(date) < date) {
+ formatFunction = d3.time('%-I %p');
+ } else if (d3.timeWeek(date) < date) {
+ formatFunction = d3.time('%a %d');
+ } else if (d3.timeMonth(date) < date) {
+ formatFunction = d3.time('%b %d');
+ } else if (d3.timeYear(date) < date) {
+ formatFunction = d3.time('%B');
+ } else {
+ formatFunction = d3.time('%Y');
+ }
+ return formatFunction(date);
+}
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index d21a265bd43..4ce3dad440c 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -1,5 +1,10 @@
-import d3 from 'd3';
import _ from 'underscore';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { line, area, curveLinear } from 'd3-shape';
+import { extent, max } from 'd3-array';
+import { timeMinute } from 'd3-time';
+
+const d3 = { scaleLinear, scaleTime, line, area, curveLinear, extent, max, timeMinute };
const defaultColorPalette = {
blue: ['#1f78d1', '#8fbce8'],
@@ -38,27 +43,27 @@ function queryTimeSeries(query, graphWidth, graphHeight, graphHeightOffset, xDom
let lineColor = '';
let areaColor = '';
- const timeSeriesScaleX = d3.time.scale()
+ const timeSeriesScaleX = d3.scaleTime()
.range([0, graphWidth - 70]);
- const timeSeriesScaleY = d3.scale.linear()
+ const timeSeriesScaleY = d3.scaleLinear()
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(xDom);
- timeSeriesScaleX.ticks(d3.time.minute, 60);
+ timeSeriesScaleX.ticks(d3.timeMinute, 60);
timeSeriesScaleY.domain(yDom);
const defined = d => !isNaN(d.value) && d.value != null;
- const lineFunction = d3.svg.line()
+ const lineFunction = d3.line()
.defined(defined)
- .interpolate('linear')
+ .curve(d3.curveLinear) // d3 v4 uses curbe instead of interpolate
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
- const areaFunction = d3.svg.area()
+ const areaFunction = d3.area()
.defined(defined)
- .interpolate('linear')
+ .curve(d3.curveLinear)
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
.y1(d => timeSeriesScaleY(d.value));
diff --git a/app/assets/javascripts/new_commit_form.js b/app/assets/javascripts/new_commit_form.js
index 6e152497d20..a2f0a44863f 100644
--- a/app/assets/javascripts/new_commit_form.js
+++ b/app/assets/javascripts/new_commit_form.js
@@ -6,11 +6,12 @@ export default class NewCommitForm {
this.branchName = form.find('.js-branch-name');
this.originalBranch = form.find('.js-original-branch');
this.createMergeRequest = form.find('.js-create-merge-request');
- this.createMergeRequestContainer = form.find('.js-create-merge-request-container');
+ this.createMergeRequestContainer = form.find(
+ '.js-create-merge-request-container',
+ );
this.branchName.keyup(this.renderDestination);
this.renderDestination();
}
-
renderDestination() {
var different;
different = this.branchName.val() !== this.originalBranch.val();
@@ -23,6 +24,6 @@ export default class NewCommitForm {
this.createMergeRequestContainer.hide();
this.createMergeRequest.prop('checked', false);
}
- return this.wasDifferent = different;
+ return (this.wasDifferent = different);
}
}
diff --git a/app/assets/javascripts/preview_markdown.js b/app/assets/javascripts/preview_markdown.js
index 141333b2b4d..ffaafb3ee9e 100644
--- a/app/assets/javascripts/preview_markdown.js
+++ b/app/assets/javascripts/preview_markdown.js
@@ -117,12 +117,10 @@
}());
markdownPreview = new window.MarkdownPreview();
-
previewButtonSelector = '.js-md-preview-button';
-
writeButtonSelector = '.js-md-write-button';
-
lastTextareaPreviewed = null;
+ const markdownToolbar = $('.md-header-toolbar');
$.fn.setupMarkdownPreview = function () {
var $form = $(this);
@@ -146,6 +144,7 @@
// toggle content
$form.find('.md-write-holder').hide();
$form.find('.md-preview-holder').show();
+ markdownToolbar.removeClass('active');
markdownPreview.showPreview($form);
});
@@ -167,6 +166,7 @@
$form.find('.md-write-holder').show();
$form.find('textarea.markdown-area').focus();
$form.find('.md-preview-holder').hide();
+ markdownToolbar.addClass('active');
markdownPreview.hideReferencedCommands($form);
});
diff --git a/app/assets/javascripts/repo/components/commit_sidebar/list.vue b/app/assets/javascripts/repo/components/commit_sidebar/list.vue
deleted file mode 100644
index fb862e7bf01..00000000000
--- a/app/assets/javascripts/repo/components/commit_sidebar/list.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
- import listItem from './list_item.vue';
- import listCollapsed from './list_collapsed.vue';
-
- export default {
- components: {
- icon,
- listItem,
- listCollapsed,
- },
- props: {
- title: {
- type: String,
- required: true,
- },
- fileList: {
- type: Array,
- required: true,
- },
- collapsed: {
- type: Boolean,
- required: true,
- },
- },
- methods: {
- toggleCollapsed() {
- this.$emit('toggleCollapsed');
- },
- },
- };
-</script>
-
-<template>
- <div class="multi-file-commit-panel-section">
- <header
- class="multi-file-commit-panel-header"
- :class="{
- 'is-collapsed': collapsed,
- }"
- >
- <icon
- name="list-bulleted"
- :size="18"
- css-classes="append-right-default"
- />
- <template v-if="!collapsed">
- {{ title }}
- <button
- type="button"
- class="btn btn-transparent multi-file-commit-panel-collapse-btn"
- @click="toggleCollapsed"
- >
- <i
- aria-hidden="true"
- class="fa fa-angle-double-right"
- >
- </i>
- </button>
- </template>
- </header>
- <div class="multi-file-commit-list">
- <list-collapsed
- v-if="collapsed"
- />
- <template v-else>
- <ul
- v-if="fileList.length"
- class="list-unstyled append-bottom-0"
- >
- <li
- v-for="file in fileList"
- :key="file.key"
- >
- <list-item
- :file="file"
- />
- </li>
- </ul>
- <div
- v-else
- class="help-block prepend-top-0"
- >
- No changes
- </div>
- </template>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/new_dropdown/index.vue b/app/assets/javascripts/repo/components/new_dropdown/index.vue
deleted file mode 100644
index 781404cf8ca..00000000000
--- a/app/assets/javascripts/repo/components/new_dropdown/index.vue
+++ /dev/null
@@ -1,89 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import newModal from './modal.vue';
- import upload from './upload.vue';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- newModal,
- upload,
- },
- data() {
- return {
- openModal: false,
- modalType: '',
- };
- },
- computed: {
- ...mapState([
- 'path',
- ]),
- },
- methods: {
- createNewItem(type) {
- this.modalType = type;
- this.toggleModalOpen();
- },
- toggleModalOpen() {
- this.openModal = !this.openModal;
- },
- },
- };
-</script>
-
-<template>
- <div>
- <ul class="breadcrumb repo-breadcrumb">
- <li class="dropdown">
- <button
- type="button"
- class="btn btn-default dropdown-toggle add-to-tree"
- data-toggle="dropdown"
- aria-label="Create new file or directory"
- >
- <icon
- name="plus"
- css-classes="pull-left"
- />
- <icon
- name="arrow-down"
- css-classes="pull-left"
- />
- </button>
- <ul class="dropdown-menu">
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('blob')"
- >
- {{ __('New file') }}
- </a>
- </li>
- <li>
- <upload
- :path="path"
- />
- </li>
- <li>
- <a
- href="#"
- role="button"
- @click.prevent="createNewItem('tree')"
- >
- {{ __('New directory') }}
- </a>
- </li>
- </ul>
- </li>
- </ul>
- <new-modal
- v-if="openModal"
- :type="modalType"
- :path="path"
- @toggle="toggleModalOpen"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo.vue b/app/assets/javascripts/repo/components/repo.vue
deleted file mode 100644
index a00e1e9d809..00000000000
--- a/app/assets/javascripts/repo/components/repo.vue
+++ /dev/null
@@ -1,63 +0,0 @@
-<script>
-import { mapState, mapGetters } from 'vuex';
-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 repoEditor from './repo_editor.vue';
-
-export default {
- computed: {
- ...mapState([
- 'currentBlobView',
- ]),
- ...mapGetters([
- 'isCollapsed',
- 'changedFiles',
- ]),
- },
- components: {
- RepoSidebar,
- RepoTabs,
- RepoFileButtons,
- repoEditor,
- RepoCommitSection,
- RepoPreview,
- },
- mounted() {
- const returnValue = 'Are you sure you want to lose unsaved changes?';
- window.onbeforeunload = (e) => {
- if (!this.changedFiles.length) return undefined;
-
- Object.assign(e, {
- returnValue,
- });
- return returnValue;
- };
- },
-};
-</script>
-
-<template>
- <div
- class="multi-file"
- :class="{
- 'is-collapsed': isCollapsed
- }"
- >
- <repo-sidebar/>
- <div
- v-if="isCollapsed"
- class="multi-file-edit-pane"
- >
- <repo-tabs />
- <component
- class="multi-file-edit-pane-content"
- :is="currentBlobView"
- />
- <repo-file-buttons />
- </div>
- <repo-commit-section />
- </div>
-</template>
diff --git a/app/assets/javascripts/repo/components/repo_sidebar.vue b/app/assets/javascripts/repo/components/repo_sidebar.vue
deleted file mode 100644
index 4ea21913129..00000000000
--- a/app/assets/javascripts/repo/components/repo_sidebar.vue
+++ /dev/null
@@ -1,85 +0,0 @@
-<script>
-import { mapState, mapGetters, mapActions } from 'vuex';
-import RepoPreviousDirectory from './repo_prev_directory.vue';
-import RepoFile from './repo_file.vue';
-import RepoLoadingFile from './repo_loading_file.vue';
-
-export default {
- components: {
- 'repo-previous-directory': RepoPreviousDirectory,
- 'repo-file': RepoFile,
- 'repo-loading-file': RepoLoadingFile,
- },
- created() {
- window.addEventListener('popstate', this.popHistoryState);
- },
- destroyed() {
- window.removeEventListener('popstate', this.popHistoryState);
- },
- mounted() {
- this.getTreeData();
- },
- computed: {
- ...mapState([
- 'loading',
- 'isRoot',
- ]),
- ...mapState({
- projectName(state) {
- return state.project.name;
- },
- }),
- ...mapGetters([
- 'treeList',
- 'isCollapsed',
- ]),
- },
- methods: {
- ...mapActions([
- 'getTreeData',
- 'popHistoryState',
- ]),
- },
-};
-</script>
-
-<template>
-<div class="ide-file-list">
- <table class="table">
- <thead>
- <tr>
- <th
- v-if="isCollapsed"
- >
- </th>
- <template v-else>
- <th class="name multi-file-table-name">
- Name
- </th>
- <th class="hidden-sm hidden-xs last-commit">
- Last commit
- </th>
- <th class="hidden-xs last-update text-right">
- Last update
- </th>
- </template>
- </tr>
- </thead>
- <tbody>
- <repo-previous-directory
- v-if="!isRoot && treeList.length"
- />
- <repo-loading-file
- v-if="!treeList.length && loading"
- v-for="n in 5"
- :key="n"
- />
- <repo-file
- v-for="file in treeList"
- :key="file.key"
- :file="file"
- />
- </tbody>
- </table>
-</div>
-</template>
diff --git a/app/assets/javascripts/repo/index.js b/app/assets/javascripts/repo/index.js
deleted file mode 100644
index b6801af7fcb..00000000000
--- a/app/assets/javascripts/repo/index.js
+++ /dev/null
@@ -1,106 +0,0 @@
-import Vue from 'vue';
-import { mapActions } from 'vuex';
-import { convertPermissionToBoolean } from '../lib/utils/common_utils';
-import Repo from './components/repo.vue';
-import RepoEditButton from './components/repo_edit_button.vue';
-import newBranchForm from './components/new_branch_form.vue';
-import newDropdown from './components/new_dropdown/index.vue';
-import store from './stores';
-import Translate from '../vue_shared/translate';
-
-function initRepo(el) {
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- components: {
- repo: Repo,
- },
- methods: {
- ...mapActions([
- 'setInitialData',
- ]),
- },
- created() {
- const data = el.dataset;
-
- this.setInitialData({
- project: {
- id: data.projectId,
- name: data.projectName,
- url: data.projectUrl,
- },
- endpoints: {
- rootEndpoint: data.url,
- newMergeRequestUrl: data.newMergeRequestUrl,
- rootUrl: data.rootUrl,
- },
- canCommit: convertPermissionToBoolean(data.canCommit),
- onTopOfBranch: convertPermissionToBoolean(data.onTopOfBranch),
- currentRef: data.ref,
- path: data.currentPath,
- currentBranch: data.currentBranch,
- isRoot: convertPermissionToBoolean(data.root),
- isInitialRoot: convertPermissionToBoolean(data.root),
- });
- },
- render(createElement) {
- return createElement('repo');
- },
- });
-}
-
-function initRepoEditButton(el) {
- return new Vue({
- el,
- store,
- components: {
- repoEditButton: RepoEditButton,
- },
- render(createElement) {
- return createElement('repo-edit-button');
- },
- });
-}
-
-function initNewDropdown(el) {
- return new Vue({
- el,
- store,
- components: {
- newDropdown,
- },
- render(createElement) {
- return createElement('new-dropdown');
- },
- });
-}
-
-function initNewBranchForm() {
- const el = document.querySelector('.js-new-branch-dropdown');
-
- if (!el) return null;
-
- return new Vue({
- el,
- components: {
- newBranchForm,
- },
- store,
- render(createElement) {
- return createElement('new-branch-form');
- },
- });
-}
-
-const repo = document.getElementById('repo');
-const editButton = document.querySelector('.editable-mode');
-const newDropdownHolder = document.querySelector('.js-new-dropdown');
-
-Vue.use(Translate);
-
-initRepo(repo);
-initRepoEditButton(editButton);
-initNewBranchForm();
-initNewDropdown(newDropdownHolder);
diff --git a/app/assets/javascripts/repo/stores/actions.js b/app/assets/javascripts/repo/stores/actions.js
deleted file mode 100644
index af5dcf054ef..00000000000
--- a/app/assets/javascripts/repo/stores/actions.js
+++ /dev/null
@@ -1,146 +0,0 @@
-import Vue from 'vue';
-import { visitUrl } from '../../lib/utils/url_utility';
-import flash from '../../flash';
-import service from '../services';
-import * as types from './mutation_types';
-
-export const redirectToUrl = (_, url) => visitUrl(url);
-
-export const setInitialData = ({ commit }, data) => commit(types.SET_INITIAL_DATA, data);
-
-export const closeDiscardPopup = ({ commit }) => commit(types.TOGGLE_DISCARD_POPUP, false);
-
-export const discardAllChanges = ({ commit, getters, dispatch }) => {
- const changedFiles = getters.changedFiles;
-
- changedFiles.forEach((file) => {
- commit(types.DISCARD_FILE_CHANGES, file);
-
- if (file.tempFile) {
- dispatch('closeFile', { file, force: true });
- }
- });
-};
-
-export const closeAllFiles = ({ state, dispatch }) => {
- state.openFiles.forEach(file => dispatch('closeFile', { file }));
-};
-
-export const toggleEditMode = ({ state, commit, getters, dispatch }, force = false) => {
- const changedFiles = getters.changedFiles;
-
- if (changedFiles.length && !force) {
- commit(types.TOGGLE_DISCARD_POPUP, true);
- } else {
- commit(types.TOGGLE_EDIT_MODE);
- commit(types.TOGGLE_DISCARD_POPUP, false);
- dispatch('toggleBlobView');
-
- if (!state.editMode) {
- dispatch('discardAllChanges');
- }
- }
-};
-
-export const toggleBlobView = ({ commit, state }) => {
- if (state.editMode) {
- commit(types.SET_EDIT_MODE);
- } else {
- commit(types.SET_PREVIEW_MODE);
- }
-};
-
-export const checkCommitStatus = ({ state }) => service.getBranchData(
- state.project.id,
- state.currentBranch,
-)
- .then((data) => {
- const { id } = data.commit;
-
- if (state.currentRef !== id) {
- return true;
- }
-
- return false;
- })
- .catch(() => flash('Error checking branch data. Please try again.'));
-
-export const commitChanges = ({ commit, state, dispatch, getters }, { payload, newMr }) =>
- service.commit(state.project.id, payload)
- .then((data) => {
- const { branch } = payload;
- if (!data.short_id) {
- flash(data.message);
- return;
- }
-
- const lastCommit = {
- commit_path: `${state.project.url}/commit/${data.id}`,
- commit: {
- message: data.message,
- authored_date: data.committed_date,
- },
- };
-
- flash(`Your changes have been committed. Commit ${data.short_id} with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`, 'notice');
-
- if (newMr) {
- dispatch('redirectToUrl', `${state.endpoints.newMergeRequestUrl}${branch}`);
- } else {
- commit(types.SET_COMMIT_REF, data.id);
-
- getters.changedFiles.forEach((entry) => {
- commit(types.SET_LAST_COMMIT_DATA, {
- entry,
- lastCommit,
- });
- });
-
- dispatch('discardAllChanges');
- dispatch('closeAllFiles');
- dispatch('toggleEditMode');
-
- window.scrollTo(0, 0);
- }
- })
- .catch(() => flash('Error committing changes. Please try again.'));
-
-export const createTempEntry = ({ state, dispatch }, { name, type, content = '', base64 = false }) => {
- if (type === 'tree') {
- dispatch('createTempTree', name);
- } else if (type === 'blob') {
- dispatch('createTempFile', {
- tree: state,
- name,
- base64,
- content,
- });
- }
-};
-
-export const popHistoryState = ({ state, dispatch, getters }) => {
- const treeList = getters.treeList;
- const tree = treeList.find(file => file.url === state.previousUrl);
-
- if (!tree) return;
-
- if (tree.type === 'tree') {
- dispatch('toggleTreeOpen', { endpoint: tree.url, tree });
- }
-};
-
-export const scrollToTab = () => {
- Vue.nextTick(() => {
- const tabs = document.getElementById('tabs');
-
- if (tabs) {
- const tabEl = tabs.querySelector('.active .repo-tab');
-
- tabEl.focus();
- }
- });
-};
-
-export * from './actions/tree';
-export * from './actions/file';
-export * from './actions/branch';
diff --git a/app/assets/javascripts/repo/stores/actions/branch.js b/app/assets/javascripts/repo/stores/actions/branch.js
deleted file mode 100644
index 61d9a5af3e3..00000000000
--- a/app/assets/javascripts/repo/stores/actions/branch.js
+++ /dev/null
@@ -1,20 +0,0 @@
-import service from '../../services';
-import * as types from '../mutation_types';
-import { pushState } from '../utils';
-
-// eslint-disable-next-line import/prefer-default-export
-export const createNewBranch = ({ state, commit }, branch) => service.createBranch(
- state.project.id,
- {
- branch,
- ref: state.currentBranch,
- },
-).then(res => res.json())
-.then((data) => {
- const branchName = data.name;
- const url = location.href.replace(state.currentBranch, branchName);
-
- pushState(url);
-
- commit(types.SET_CURRENT_BRANCH, branchName);
-});
diff --git a/app/assets/javascripts/repo/stores/actions/tree.js b/app/assets/javascripts/repo/stores/actions/tree.js
deleted file mode 100644
index 7c251e26bed..00000000000
--- a/app/assets/javascripts/repo/stores/actions/tree.js
+++ /dev/null
@@ -1,163 +0,0 @@
-import { visitUrl } from '../../../lib/utils/url_utility';
-import { normalizeHeaders } from '../../../lib/utils/common_utils';
-import flash from '../../../flash';
-import service from '../../services';
-import * as types from '../mutation_types';
-import {
- pushState,
- setPageTitle,
- findEntry,
- createTemp,
- createOrMergeEntry,
-} from '../utils';
-
-export const getTreeData = (
- { commit, state, dispatch },
- { endpoint = state.endpoints.rootEndpoint, tree = state } = {},
-) => {
- commit(types.TOGGLE_LOADING, tree);
-
- service.getTreeData(endpoint)
- .then((res) => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
- setPageTitle(pageTitle);
-
- return res.json();
- })
- .then((data) => {
- const prevLastCommitPath = tree.lastCommitPath;
- if (!state.isInitialRoot) {
- commit(types.SET_ROOT, data.path === '/');
- }
-
- dispatch('updateDirectoryData', { data, tree });
- commit(types.SET_PARENT_TREE_URL, data.parent_tree_url);
- commit(types.SET_LAST_COMMIT_URL, { tree, url: data.last_commit_path });
- commit(types.TOGGLE_LOADING, tree);
-
- if (prevLastCommitPath !== null) {
- dispatch('getLastCommitData', tree);
- }
-
- pushState(endpoint);
- })
- .catch(() => {
- flash('Error loading tree data. Please try again.');
- commit(types.TOGGLE_LOADING, tree);
- });
-};
-
-export const toggleTreeOpen = ({ commit, dispatch }, { endpoint, tree }) => {
- if (tree.opened) {
- // send empty data to clear the tree
- const data = { trees: [], blobs: [], submodules: [] };
-
- pushState(tree.parentTreeUrl);
-
- commit(types.SET_PREVIOUS_URL, tree.parentTreeUrl);
- dispatch('updateDirectoryData', { data, tree });
- } else {
- commit(types.SET_PREVIOUS_URL, endpoint);
- dispatch('getTreeData', { endpoint, tree });
- }
-
- commit(types.TOGGLE_TREE_OPEN, tree);
-};
-
-export const clickedTreeRow = ({ commit, dispatch }, row) => {
- if (row.type === 'tree') {
- dispatch('toggleTreeOpen', {
- endpoint: row.url,
- tree: row,
- });
- } else if (row.type === 'submodule') {
- commit(types.TOGGLE_LOADING, row);
-
- visitUrl(row.url);
- } else if (row.type === 'blob' && row.opened) {
- dispatch('setFileActive', row);
- } else {
- dispatch('getFileData', row);
- }
-};
-
-export const createTempTree = ({ state, commit, dispatch }, name) => {
- let tree = state;
- const dirNames = name.replace(new RegExp(`^${state.path}/`), '').split('/');
-
- dirNames.forEach((dirName) => {
- const foundEntry = findEntry(tree, 'tree', dirName);
-
- if (!foundEntry) {
- const tmpEntry = createTemp({
- name: dirName,
- path: tree.path,
- type: 'tree',
- level: tree.level !== undefined ? tree.level + 1 : 0,
- });
-
- commit(types.CREATE_TMP_TREE, {
- parent: tree,
- tmpEntry,
- });
- commit(types.TOGGLE_TREE_OPEN, tmpEntry);
-
- tree = tmpEntry;
- } else {
- tree = foundEntry;
- }
- });
-
- if (tree.tempFile) {
- dispatch('createTempFile', {
- tree,
- name: '.gitkeep',
- });
- }
-};
-
-export const getLastCommitData = ({ state, commit, dispatch, getters }, tree = state) => {
- if (tree.lastCommitPath === null || getters.isCollapsed) return;
-
- service.getTreeLastCommit(tree.lastCommitPath)
- .then((res) => {
- const lastCommitPath = normalizeHeaders(res.headers)['MORE-LOGS-URL'] || null;
-
- commit(types.SET_LAST_COMMIT_URL, { tree, url: lastCommitPath });
-
- return res.json();
- })
- .then((data) => {
- data.forEach((lastCommit) => {
- const entry = findEntry(tree, lastCommit.type, lastCommit.file_name);
-
- if (entry) {
- commit(types.SET_LAST_COMMIT_DATA, { entry, lastCommit });
- }
- });
-
- dispatch('getLastCommitData', tree);
- })
- .catch(() => flash('Error fetching log data.'));
-};
-
-export const updateDirectoryData = ({ commit, state }, { data, tree }) => {
- const level = tree.level !== undefined ? tree.level + 1 : 0;
- const parentTreeUrl = data.parent_tree_url ? `${data.parent_tree_url}${data.path}` : state.endpoints.rootUrl;
- const createEntry = (entry, type) => createOrMergeEntry({
- tree,
- entry,
- level,
- type,
- parentTreeUrl,
- });
-
- const formattedData = [
- ...data.trees.map(t => createEntry(t, 'tree')),
- ...data.submodules.map(m => createEntry(m, 'submodule')),
- ...data.blobs.map(b => createEntry(b, 'blob')),
- ];
-
- commit(types.SET_DIRECTORY_DATA, { tree, data: formattedData });
-};
diff --git a/app/assets/javascripts/repo/stores/getters.js b/app/assets/javascripts/repo/stores/getters.js
deleted file mode 100644
index 5ce9f449905..00000000000
--- a/app/assets/javascripts/repo/stores/getters.js
+++ /dev/null
@@ -1,40 +0,0 @@
-import _ from 'underscore';
-
-/*
- Takes the multi-dimensional tree and returns a flattened array.
- This allows for the table to recursively render the table rows but keeps the data
- structure nested to make it easier to add new files/directories.
-*/
-export const treeList = (state) => {
- const mapTree = arr => (!arr.tree.length ? [] : _.map(arr.tree, a => [a, mapTree(a)]));
-
- return _.chain(state.tree)
- .map(arr => [arr, mapTree(arr)])
- .flatten()
- .value();
-};
-
-export const changedFiles = state => state.openFiles.filter(file => file.changed);
-
-export const activeFile = state => state.openFiles.find(file => file.active);
-
-export const activeFileExtension = (state) => {
- const file = activeFile(state);
- return file ? `.${file.path.split('.').pop()}` : '';
-};
-
-export const isCollapsed = state => !!state.openFiles.length;
-
-export const canEditFile = (state) => {
- const currentActiveFile = activeFile(state);
- const openedFiles = state.openFiles;
-
- return state.canCommit &&
- state.onTopOfBranch &&
- openedFiles.length &&
- (currentActiveFile && !currentActiveFile.renderError && !currentActiveFile.binary);
-};
-
-export const addedFiles = state => changedFiles(state).filter(f => f.tempFile);
-
-export const modifiedFiles = state => changedFiles(state).filter(f => !f.tempFile);
diff --git a/app/assets/javascripts/repo/stores/mutations/branch.js b/app/assets/javascripts/repo/stores/mutations/branch.js
deleted file mode 100644
index d8229e8a620..00000000000
--- a/app/assets/javascripts/repo/stores/mutations/branch.js
+++ /dev/null
@@ -1,9 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.SET_CURRENT_BRANCH](state, currentBranch) {
- Object.assign(state, {
- currentBranch,
- });
- },
-};
diff --git a/app/assets/javascripts/shortcuts.js b/app/assets/javascripts/shortcuts.js
index 130730b1700..d2f0d7410da 100644
--- a/app/assets/javascripts/shortcuts.js
+++ b/app/assets/javascripts/shortcuts.js
@@ -51,7 +51,10 @@ export default class Shortcuts {
}
onToggleHelp(e) {
- e.preventDefault();
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
+
Shortcuts.toggleHelp(this.enabledHelp);
}
@@ -112,6 +115,9 @@ export default class Shortcuts {
static focusSearch(e) {
$('#search').focus();
- e.preventDefault();
+
+ if (e.preventDefault) {
+ e.preventDefault();
+ }
}
}
diff --git a/app/assets/javascripts/users/activity_calendar.js b/app/assets/javascripts/users/activity_calendar.js
index 4fa8c680580..0581239d5a5 100644
--- a/app/assets/javascripts/users/activity_calendar.js
+++ b/app/assets/javascripts/users/activity_calendar.js
@@ -1,7 +1,10 @@
import _ from 'underscore';
-import d3 from 'd3';
+import { scaleLinear, scaleThreshold } from 'd3-scale';
+import { select } from 'd3-selection';
import { getDayName, getDayDifference } from '../lib/utils/datetime_utility';
+const d3 = { select, scaleLinear, scaleThreshold };
+
const LOADING_HTML = `
<div class="text-center">
<i class="fa fa-spinner fa-spin user-calendar-activities-loading"></i>
@@ -28,7 +31,7 @@ function formatTooltipText({ date, count }) {
return `${contribText}<br />${dateDayName} ${dateText}`;
}
-const initColorKey = () => d3.scale.linear().range(['#acd5f2', '#254e77']).domain([0, 3]);
+const initColorKey = () => d3.scaleLinear().range(['#acd5f2', '#254e77']).domain([0, 3]);
export default class ActivityCalendar {
constructor(container, timestamps, calendarActivitiesPath, utcOffset = 0) {
@@ -205,7 +208,7 @@ export default class ActivityCalendar {
initColor() {
const colorRange = ['#ededed', this.colorKey(0), this.colorKey(1), this.colorKey(2), this.colorKey(3)];
- return d3.scale.threshold().domain([0, 10, 20, 30]).range(colorRange);
+ return d3.scaleThreshold().domain([0, 10, 20, 30]).range(colorRange);
}
clickDay(stamp) {
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 9cb3edead86..8a9129c385b 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
@@ -62,7 +62,7 @@ export default {
return this.mr.hasCI;
},
shouldRenderRelatedLinks() {
- return !!this.mr.relatedLinks;
+ return !!this.mr.relatedLinks && !this.mr.isNothingToMergeState;
},
shouldRenderDeployments() {
return this.mr.deployments.length;
diff --git a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
index 7c15abfff10..2bace3311c8 100644
--- a/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
+++ b/app/assets/javascripts/vue_merge_request_widget/stores/get_state_key.js
@@ -1,30 +1,32 @@
+import { stateKey } from './state_maps';
+
export default function deviseState(data) {
if (data.project_archived) {
- return 'archived';
+ return stateKey.archived;
} else if (data.branch_missing) {
- return 'missingBranch';
+ return stateKey.missingBranch;
} else if (!data.commits_count) {
- return 'nothingToMerge';
+ return stateKey.nothingToMerge;
} else if (this.mergeStatus === 'unchecked') {
- return 'checking';
+ return stateKey.checking;
} else if (data.has_conflicts) {
- return 'conflicts';
+ return stateKey.conflicts;
} else if (data.work_in_progress) {
- return 'workInProgress';
+ return stateKey.workInProgress;
} else if (this.onlyAllowMergeIfPipelineSucceeds && this.isPipelineFailed) {
- return 'pipelineFailed';
+ return stateKey.pipelineFailed;
} else if (this.hasMergeableDiscussionsState) {
- return 'unresolvedDiscussions';
+ return stateKey.unresolvedDiscussions;
} else if (this.isPipelineBlocked) {
- return 'pipelineBlocked';
+ return stateKey.pipelineBlocked;
} else if (this.hasSHAChanged) {
- return 'shaMismatch';
+ return stateKey.shaMismatch;
} else if (this.mergeWhenPipelineSucceeds) {
- return this.mergeError ? 'autoMergeFailed' : 'mergeWhenPipelineSucceeds';
+ return this.mergeError ? stateKey.autoMergeFailed : stateKey.mergeWhenPipelineSucceeds;
} else if (!this.canMerge) {
- return 'notAllowedToMerge';
+ return stateKey.notAllowedToMerge;
} else if (this.canBeMerged) {
- return 'readyToMerge';
+ return stateKey.readyToMerge;
}
return null;
}
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 707766e08e4..93d31a2a684 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
@@ -1,5 +1,6 @@
import Timeago from 'timeago.js';
import { getStateKey } from '../dependencies';
+import { stateKey } from './state_maps';
import { formatDate } from '../../lib/utils/datetime_utility';
export default class MergeRequestStore {
@@ -120,6 +121,10 @@ export default class MergeRequestStore {
}
}
+ get isNothingToMergeState() {
+ return this.state === stateKey.nothingToMerge;
+ }
+
static getEventObject(event) {
return {
author: MergeRequestStore.getAuthorObject(event),
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 9074a064a6d..de980c175fb 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
@@ -31,6 +31,23 @@ const statesToShowHelpWidget = [
'autoMergeFailed',
];
+export const stateKey = {
+ archived: 'archived',
+ missingBranch: 'missingBranch',
+ nothingToMerge: 'nothingToMerge',
+ checking: 'checking',
+ conflicts: 'conflicts',
+ workInProgress: 'workInProgress',
+ pipelineFailed: 'pipelineFailed',
+ unresolvedDiscussions: 'unresolvedDiscussions',
+ pipelineBlocked: 'pipelineBlocked',
+ shaMismatch: 'shaMismatch',
+ autoMergeFailed: 'autoMergeFailed',
+ mergeWhenPipelineSucceeds: 'mergeWhenPipelineSucceeds',
+ notAllowedToMerge: 'notAllowedToMerge',
+ readyToMerge: 'readyToMerge',
+};
+
export default {
stateToComponentMap,
statesToShowHelpWidget,
diff --git a/app/assets/javascripts/vue_shared/components/markdown/header.vue b/app/assets/javascripts/vue_shared/components/markdown/header.vue
index 6c575d8eb49..36d2d1dc164 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/header.vue
@@ -72,7 +72,9 @@
Preview
</a>
</li>
- <li class="md-header-toolbar">
+ <li
+ class="md-header-toolbar"
+ :class="{ active: !previewMarkdown }">
<toolbar-button
tag="**"
button-title="Add bold text"
diff --git a/app/assets/javascripts/vue_shared/components/project_avatar/image.vue b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
new file mode 100644
index 00000000000..dce23bd65f6
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/project_avatar/image.vue
@@ -0,0 +1,103 @@
+<script>
+
+/* This is a re-usable vue component for rendering a project avatar that
+ does not need to link to the project's profile. The image and an optional
+ tooltip can be configured by props passed to this component.
+
+ Sample configuration:
+
+ <project-avatar-image
+ :lazy="true"
+ :img-src="projectAvatarSrc"
+ :img-alt="tooltipText"
+ :tooltip-text="tooltipText"
+ tooltip-placement="top"
+ />
+
+*/
+
+import defaultAvatarUrl from 'images/no_avatar.png';
+import { placeholderImage } from '../../../lazy_loader';
+import tooltip from '../../directives/tooltip';
+
+export default {
+ name: 'ProjectAvatarImage',
+ props: {
+ lazy: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ imgSrc: {
+ type: String,
+ required: false,
+ default: defaultAvatarUrl,
+ },
+ cssClasses: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ imgAlt: {
+ type: String,
+ required: false,
+ default: 'project avatar',
+ },
+ size: {
+ type: Number,
+ required: false,
+ default: 20,
+ },
+ tooltipText: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ tooltipPlacement: {
+ type: String,
+ required: false,
+ default: 'top',
+ },
+ },
+ directives: {
+ tooltip,
+ },
+ computed: {
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside project avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ sanitizedSource() {
+ return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ },
+ resultantSrcAttribute() {
+ return this.lazy ? placeholderImage : this.sanitizedSource;
+ },
+ tooltipContainer() {
+ return this.tooltipText ? 'body' : null;
+ },
+ avatarSizeClass() {
+ return `s${this.size}`;
+ },
+ },
+};
+</script>
+
+<template>
+ <img
+ v-tooltip
+ class="avatar"
+ :class="{
+ lazy,
+ [avatarSizeClass]: true,
+ [cssClasses]: true
+ }"
+ :src="resultantSrcAttribute"
+ :width="size"
+ :height="size"
+ :alt="imgAlt"
+ :data-src="sanitizedSource"
+ :data-container="tooltipContainer"
+ :data-placement="tooltipPlacement"
+ :title="tooltipText"
+ />
+</template>
diff --git a/app/assets/stylesheets/framework/contextual-sidebar.scss b/app/assets/stylesheets/framework/contextual-sidebar.scss
index 8baf7ca23a4..5da06b90113 100644
--- a/app/assets/stylesheets/framework/contextual-sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual-sidebar.scss
@@ -9,12 +9,6 @@
padding-left: $contextual-sidebar-width;
}
- // Override position: absolute
- .right-sidebar {
- position: fixed;
- height: calc(100% - #{$header-height});
- }
-
.issues-bulk-update.right-sidebar.right-sidebar-expanded .issuable-sidebar-header {
padding: 10px 0 15px;
}
@@ -29,7 +23,6 @@
.context-header {
position: relative;
margin-right: 2px;
- width: $contextual-sidebar-width;
a {
transition: padding $sidebar-transition-duration;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index 478269f3fcf..bc907a390d8 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -16,27 +16,18 @@
@mixin set-visible {
transform: translateY(0);
- visibility: visible;
- opacity: 1;
- transition-duration: 100ms, 150ms, 25ms;
- transition-delay: 35ms, 50ms, 25ms;
+ display: block;
}
@mixin set-invisible {
transform: translateY(-10px);
- visibility: hidden;
- opacity: 0;
- transition-property: opacity, transform, visibility;
- transition-duration: 70ms, 250ms, 250ms;
- transition-timing-function: linear, $dropdown-animation-timing;
- transition-delay: 25ms, 50ms, 0ms;
+ display: none;
}
.open {
.dropdown-menu,
.dropdown-menu-nav {
@include set-visible;
- display: block;
min-height: 40px;
@media (max-width: $screen-xs-max) {
@@ -55,6 +46,11 @@
}
}
+// Get search dropdown to line up with other nav dropdowns
+.search-input-container .dropdown-menu {
+ margin-top: 11px;
+}
+
.dropdown-toggle {
padding: 6px 8px 6px 10px;
background-color: $white-light;
@@ -214,7 +210,6 @@
.dropdown-menu,
.dropdown-menu-nav {
@include set-invisible;
- display: block;
position: absolute;
width: auto;
top: 100%;
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 5389eb0a5f2..6b07ffdbd61 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -74,7 +74,7 @@
}
.md-header-tab {
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
flex: 1;
width: 100%;
border-bottom: 1px solid $border-color;
@@ -82,16 +82,23 @@
}
}
-.md-header-toolbar {
- margin-left: auto;
+.nav-links {
+ li.md-header-toolbar {
+ margin-left: auto;
+ display: none;
- @media(max-width: $screen-xs-max) {
- flex: none;
- display: flex;
- justify-content: center;
- width: 100%;
- padding-top: $gl-padding-top;
- padding-bottom: $gl-padding-top;
+ &.active {
+ display: block;
+
+ @media (max-width: $screen-xs-max) {
+ flex: none;
+ display: flex;
+ justify-content: center;
+ width: 100%;
+ padding-top: $gl-padding-top;
+ padding-bottom: $gl-padding-top;
+ }
+ }
}
}
@@ -175,7 +182,7 @@
margin-left: $gl-padding;
margin-right: -5px;
- @media(max-width: $screen-xs-max) {
+ @media (max-width: $screen-xs-max) {
margin-left: 0;
margin-right: 0;
}
@@ -239,7 +246,7 @@
}
}
-@media(max-width: $screen-xs-max) {
+@media (max-width: $screen-xs-max) {
.atwho-view-ul {
width: 350px;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 0742c0a2a09..d61809cb0a4 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -90,11 +90,6 @@
.right-sidebar {
border-left: 1px solid $border-color;
height: calc(100% - #{$header-height});
-
- &.affix {
- position: fixed;
- top: $header-height;
- }
}
.with-performance-bar .right-sidebar.affix {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index b84d6c140be..1d6c7a5c472 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -219,6 +219,7 @@ $gl-input-padding: 10px;
$gl-vert-padding: 6px;
$gl-padding-top: 10px;
$gl-sidebar-padding: 22px;
+$gl-bar-padding: 3px;
/*
* Misc
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index e19196e0c41..e1637618ab2 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -122,7 +122,7 @@
}
.right-sidebar {
- position: absolute;
+ position: fixed;
top: $header-height;
bottom: 0;
right: 0;
@@ -502,7 +502,7 @@
top: $header-height + $performance-bar-height;
.issuable-sidebar {
- height: calc(100% - #{$header-height} - #{$performance-bar-height});
+ height: calc(100% - #{$performance-bar-height});
}
}
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index 6eb92c7baee..da3c2d7fa5d 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -22,9 +22,10 @@
}
}
-.multi-file {
+.ide-view {
display: flex;
- height: calc(100vh - 145px);
+ height: calc(100vh - #{$header-height});
+ color: $almost-black;
border-top: 1px solid $white-dark;
border-bottom: 1px solid $white-dark;
@@ -35,12 +36,47 @@
}
}
+.with-performance-bar .ide-view {
+ height: calc(100vh - #{$header-height});
+}
+
.ide-file-list {
flex: 1;
- overflow: scroll;
.file {
cursor: pointer;
+
+ &.file-open {
+ background: $white-normal;
+ }
+
+ .repo-file-name {
+ white-space: nowrap;
+ text-overflow: ellipsis;
+ }
+
+ .unsaved-icon {
+ color: $indigo-700;
+ float: right;
+ font-size: smaller;
+ line-height: 20px;
+ }
+
+ .repo-new-btn {
+ display: none;
+ margin-top: -4px;
+ margin-bottom: -4px;
+ }
+
+ &:hover {
+ .repo-new-btn {
+ display: block;
+ }
+
+ .unsaved-icon {
+ display: none;
+ }
+ }
}
a {
@@ -55,10 +91,9 @@
.multi-file-table-name,
.multi-file-table-col-commit-message {
- white-space: nowrap;
- overflow: hidden;
- text-overflow: ellipsis;
+ overflow: visible;
max-width: 0;
+ padding: 6px 12px;
}
.multi-file-table-name {
@@ -66,6 +101,7 @@
}
.multi-file-table-col-commit-message {
+ white-space: nowrap;
width: 50%;
}
@@ -79,7 +115,7 @@
.multi-file-tabs {
display: flex;
- overflow: scroll;
+ overflow-x: auto;
background-color: $white-normal;
box-shadow: inset 0 -1px $white-dark;
@@ -128,9 +164,38 @@
height: 0;
}
+.blob-editor-container {
+ flex: 1;
+ height: 0;
+ display: flex;
+ flex-direction: column;
+ justify-content: center;
+
+ .vertical-center {
+ min-height: auto;
+ }
+}
+
+.multi-file-editor-holder {
+ height: 100%;
+}
+
.multi-file-editor-btn-group {
- padding: $grid-size;
+ padding: $gl-bar-padding $gl-padding;
border-top: 1px solid $white-dark;
+ border-bottom: 1px solid $white-dark;
+ background: $white-light;
+}
+
+.ide-status-bar {
+ padding: $gl-bar-padding $gl-padding;
+ background: $white-light;
+ display: flex;
+ justify-content: space-between;
+
+ svg {
+ vertical-align: middle;
+ }
}
// Not great, but this is to deal with our current output
@@ -138,10 +203,6 @@
height: 100%;
overflow: scroll;
- .blob-viewer {
- height: 100%;
- }
-
.file-content.code {
display: flex;
@@ -162,18 +223,101 @@
}
}
+.file-content.blob-no-preview {
+ a {
+ margin-left: auto;
+ margin-right: auto;
+ }
+}
+
.multi-file-commit-panel {
display: flex;
flex-direction: column;
height: 100%;
width: 290px;
- padding: $gl-padding;
+ padding: 0;
background-color: $gray-light;
border-left: 1px solid $white-dark;
+ .projects-sidebar {
+ display: flex;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ }
+
+ .multi-file-commit-panel-inner-scroll {
+ display: flex;
+ flex: 1;
+ flex-direction: column;
+ overflow: auto;
+ }
+
&.is-collapsed {
width: 60px;
- padding: 0;
+
+ .multi-file-commit-list {
+ padding-top: $gl-padding;
+ overflow: hidden;
+ }
+
+ .multi-file-context-bar-icon {
+ align-items: center;
+
+ svg {
+ float: none;
+ margin: 0;
+ }
+ }
+ }
+
+ .branch-container {
+ border-left: 4px solid $indigo-700;
+ margin-bottom: $gl-bar-padding;
+ }
+
+ .branch-header {
+ background: $white-dark;
+ display: flex;
+ }
+
+ .branch-header-title {
+ flex: 1;
+ padding: $grid-size $gl-padding;
+ color: $indigo-700;
+ font-weight: $gl-font-weight-bold;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+
+ .branch-header-btns {
+ padding: $gl-vert-padding $gl-padding;
+ }
+
+ .left-collapse-btn {
+ display: none;
+ background: $gray-light;
+ text-align: left;
+ border-top: 1px solid $white-dark;
+
+ svg {
+ vertical-align: middle;
+ }
+ }
+}
+
+.multi-file-context-bar-icon {
+ padding: 10px;
+
+ svg {
+ margin-right: 10px;
+ float: left;
}
}
@@ -186,9 +330,9 @@
.multi-file-commit-panel-header {
display: flex;
align-items: center;
- padding: 0 0 12px;
margin-bottom: 12px;
border-bottom: 1px solid $white-dark;
+ padding: $gl-btn-padding 0;
&.is-collapsed {
border-bottom: 1px solid $white-dark;
@@ -197,23 +341,33 @@
margin-left: auto;
margin-right: auto;
}
+
+ .multi-file-commit-panel-collapse-btn {
+ margin-right: auto;
+ margin-left: auto;
+ border-left: 0;
+ }
}
}
-.multi-file-commit-panel-collapse-btn {
- padding-top: 0;
- padding-bottom: 0;
- margin-left: auto;
- font-size: 20px;
+.multi-file-commit-panel-header-title {
+ display: flex;
+ flex: 1;
+ padding: $gl-btn-padding;
- &.is-collapsed {
- margin-right: auto;
+ svg {
+ margin-right: $gl-btn-padding;
}
}
+.multi-file-commit-panel-collapse-btn {
+ border-left: 1px solid $white-dark;
+}
+
.multi-file-commit-list {
flex: 1;
- overflow: scroll;
+ overflow: auto;
+ padding: $gl-padding;
}
.multi-file-commit-list-item {
@@ -244,7 +398,7 @@
}
.multi-file-commit-form {
- padding-top: 12px;
+ padding: $gl-padding;
border-top: 1px solid $white-dark;
}
@@ -295,3 +449,40 @@
}
}
}
+
+.ide-loading {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.ide-empty-state {
+ display: flex;
+ height: 100vh;
+ align-items: center;
+ justify-content: center;
+}
+
+.repo-new-btn {
+ .dropdown-toggle svg {
+ margin-top: -2px;
+ margin-bottom: 2px;
+ }
+
+ .dropdown-menu {
+ left: auto;
+ right: 0;
+
+ label {
+ font-weight: $gl-font-weight-normal;
+ padding: 5px 8px;
+ margin-bottom: 0;
+ }
+ }
+}
+
+.ide-flash-container.flash-container {
+ margin-top: $header-height;
+ margin-bottom: 0;
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 49c8e546bf2..c9363188505 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -108,13 +108,6 @@ input[type="checkbox"]:hover {
// Custom dropdown positioning
.dropdown-menu {
- transition-property: opacity, transform;
- transition-duration: 250ms, 250ms;
- transition-delay: 0ms, 25ms;
- transition-timing-function: $dropdown-animation-timing;
- transform: translateY(0);
- opacity: 0;
- display: block;
left: -5px;
}
@@ -152,13 +145,6 @@ input[type="checkbox"]:hover {
background-color: $nav-badge-bg;
border-color: $border-color;
}
-
- .dropdown-menu {
- transition-duration: 100ms, 75ms;
- transition-delay: 75ms, 100ms;
- transform: translateY(7px);
- opacity: 1;
- }
}
&.has-value {
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index cede147d559..8e2c42c1bd3 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -10,7 +10,6 @@
}
.axis {
- fill: $stat-graph-axis-fill;
font-size: 10px;
}
@@ -54,9 +53,7 @@
}
.selection rect {
- fill: $stat-graph-selection-fill;
fill-opacity: 0.1;
- stroke: $stat-graph-selection-stroke;
stroke-width: 1px;
stroke-opacity: 0.4;
shape-rendering: crispedges;
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index cde1e284d2d..86bade49ec9 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -8,12 +8,12 @@ class AutocompleteController < ApplicationController
def users
@users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
- render json: @users, only: [:name, :username, :id], methods: [:avatar_url]
+ render json: UserSerializer.new.represent(@users)
end
def user
@user = User.find(params[:id])
- render json: @user, only: [:name, :username, :id], methods: [:avatar_url]
+ render json: UserSerializer.new.represent(@user)
end
def projects
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
new file mode 100644
index 00000000000..1ff25a45398
--- /dev/null
+++ b/app/controllers/ide_controller.rb
@@ -0,0 +1,6 @@
+class IdeController < ApplicationController
+ layout 'nav_only'
+
+ def index
+ end
+end
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 770381472c5..d838b8dc29e 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -5,9 +5,6 @@ class Projects::BlobController < Projects::ApplicationController
include RendersBlob
include ActionView::Helpers::SanitizeHelper
- # Raised when given an invalid file path
- InvalidPathError = Class.new(StandardError)
-
prepend_before_action :authenticate_user!, only: [:edit]
before_action :require_non_empty_project, except: [:new, :create]
@@ -61,7 +58,6 @@ class Projects::BlobController < Projects::ApplicationController
create_commit(Files::UpdateService, success_path: -> { after_edit_path },
failure_view: :edit,
failure_path: project_blob_path(@project, @id))
-
rescue Files::UpdateService::FileChangedError
@conflict = true
render :edit
@@ -132,7 +128,6 @@ class Projects::BlobController < Projects::ApplicationController
def assign_blob_vars
@id = params[:id]
@ref, @path = extract_ref(@id)
-
rescue InvalidPathError
render_404
end
diff --git a/app/controllers/projects/clusters/gcp_controller.rb b/app/controllers/projects/clusters/gcp_controller.rb
index b64f7a2a6bd..d3b9d8a9bbc 100644
--- a/app/controllers/projects/clusters/gcp_controller.rb
+++ b/app/controllers/projects/clusters/gcp_controller.rb
@@ -39,6 +39,7 @@ class Projects::Clusters::GcpController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
provider_gcp_attributes: [
:gcp_project_id,
:zone,
diff --git a/app/controllers/projects/clusters/user_controller.rb b/app/controllers/projects/clusters/user_controller.rb
index d7678512073..d0db64b2fa9 100644
--- a/app/controllers/projects/clusters/user_controller.rb
+++ b/app/controllers/projects/clusters/user_controller.rb
@@ -26,6 +26,7 @@ class Projects::Clusters::UserController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
platform_kubernetes_attributes: [
:namespace,
:api_url,
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 4a7879db313..1dc7f1b3a7f 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -87,6 +87,7 @@ class Projects::ClustersController < Projects::ApplicationController
if cluster.managed?
params.require(:cluster).permit(
:enabled,
+ :environment_scope,
platform_kubernetes_attributes: [
:namespace
]
@@ -95,6 +96,7 @@ class Projects::ClustersController < Projects::ApplicationController
params.require(:cluster).permit(
:enabled,
:name,
+ :environment_scope,
platform_kubernetes_attributes: [
:api_url,
:token,
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 7ad7b3003af..e146d0d3cd5 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -29,6 +29,8 @@ class Projects::PipelinesController < Projects::ApplicationController
@pipelines_count = PipelinesFinder
.new(project).execute.count
+ @pipelines.map(&:commit) # List commits for batch loading
+
respond_to do |format|
format.html
format.json do
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 4754a67450f..d13407a06c8 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -306,7 +306,7 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true"
end
- def show_new_repo?
+ def show_new_ide?
cookies["new_repo"] == "true" && body_data_page != 'projects:show'
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 556ed233ccf..3c2ee2cb5bc 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -8,7 +8,7 @@ module BlobHelper
%w(credits changelog news copying copyright license authors)
end
- def edit_path(project = @project, ref = @ref, path = @path, options = {})
+ def edit_blob_path(project = @project, ref = @ref, path = @path, options = {})
project_edit_blob_path(project,
tree_join(ref, path),
options[:link_opts])
@@ -26,10 +26,10 @@ module BlobHelper
button_tag 'Edit', class: "#{common_classes} disabled has-tooltip", title: "You can only edit files when you are on a branch", data: { container: 'body' }
# This condition applies to anonymous or users who can edit directly
elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
- link_to 'Edit', edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
+ link_to 'Edit', edit_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
elsif current_user && can?(current_user, :fork_project, project)
continue_params = {
- to: edit_path(project, ref, path, options),
+ to: edit_blob_path(project, ref, path, options),
notice: edit_in_new_fork_notice,
notice_now: edit_in_new_fork_notice_now
}
@@ -41,6 +41,43 @@ module BlobHelper
end
end
+ def ide_edit_path(project = @project, ref = @ref, path = @path, options = {})
+ "#{ide_path}/project#{edit_blob_path(project, ref, path, options)}"
+ end
+
+ def ide_edit_text
+ "#{_('Multi Edit')} <span class='label label-primary'>#{_('Beta')}</span>".html_safe
+ end
+
+ def ide_blob_link(project = @project, ref = @ref, path = @path, options = {})
+ return unless show_new_ide?
+
+ blob = options.delete(:blob)
+ blob ||= project.repository.blob_at(ref, path) rescue nil
+
+ return unless blob && blob.readable_text?
+
+ common_classes = "btn js-edit-ide #{options[:extra_class]}"
+
+ if !on_top_of_branch?(project, ref)
+ button_tag ide_edit_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' }
+ # This condition applies to anonymous or users who can edit directly
+ elsif current_user && can_modify_blob?(blob, project, ref)
+ link_to ide_edit_text, ide_edit_path(project, ref, path, options), class: "#{common_classes} btn-sm"
+ elsif current_user && can?(current_user, :fork_project, project)
+ continue_params = {
+ to: ide_edit_path(project, ref, path, options),
+ 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)
+
+ button_tag ide_edit_text,
+ class: common_classes,
+ data: { fork_path: fork_path }
+ end
+ end
+
def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
diff --git a/app/helpers/clusters_helper.rb b/app/helpers/clusters_helper.rb
new file mode 100644
index 00000000000..7e4eb06b99d
--- /dev/null
+++ b/app/helpers/clusters_helper.rb
@@ -0,0 +1,5 @@
+module ClustersHelper
+ def has_multiple_clusters?(project)
+ false
+ end
+end
diff --git a/app/helpers/form_helper.rb b/app/helpers/form_helper.rb
index b5dece38de1..e26ce6da030 100644
--- a/app/helpers/form_helper.rb
+++ b/app/helpers/form_helper.rb
@@ -35,7 +35,7 @@ module FormHelper
multi_select: true,
'input-meta': 'name',
'always-show-selectbox': true,
- current_user_info: current_user.to_json(only: [:id, :name])
+ current_user_info: UserSerializer.new.represent(current_user)
}
}
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index b4ca0e68e0b..2668cf78afe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -362,7 +362,7 @@ module IssuablesHelper
moveIssueEndpoint: move_namespace_project_issue_path(namespace_id: issuable.project.namespace.to_param, project_id: issuable.project, id: issuable),
projectsAutocompleteEndpoint: autocomplete_projects_path(project_id: @project.id),
editable: can_edit_issuable,
- currentUser: current_user.as_json(only: [:username, :id, :name], methods: :avatar_url),
+ currentUser: UserSerializer.new.represent(current_user),
rootPath: root_path,
fullPath: @project.full_path
}
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 2f57660516d..0f9ac958f95 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -139,7 +139,7 @@ module SearchHelper
id: "filtered-search-#{type}",
placeholder: 'Search or filter results...',
data: {
- 'username-params' => @users.to_json(only: [:id, :username])
+ 'username-params' => UserSerializer.new.represent(@users)
},
autocomplete: 'off'
}
diff --git a/app/helpers/sorting_helper.rb b/app/helpers/sorting_helper.rb
index b05eb93b465..36a311dfa8a 100644
--- a/app/helpers/sorting_helper.rb
+++ b/app/helpers/sorting_helper.rb
@@ -43,14 +43,20 @@ module SortingHelper
end
def groups_sort_options_hash
- options = {
+ {
+ sort_value_name => sort_title_name,
+ sort_value_name_desc => sort_title_name_desc,
sort_value_recently_created => sort_title_recently_created,
sort_value_oldest_created => sort_title_oldest_created,
sort_value_recently_updated => sort_title_recently_updated,
sort_value_oldest_updated => sort_title_oldest_updated
}
+ end
- options
+ def admin_groups_sort_options_hash
+ groups_sort_options_hash.merge(
+ sort_value_largest_group => sort_title_largest_group
+ )
end
def member_sort_options_hash
diff --git a/app/models/blob_viewer/dependency_manager.rb b/app/models/blob_viewer/dependency_manager.rb
index a8d9be945dc..cc4950240af 100644
--- a/app/models/blob_viewer/dependency_manager.rb
+++ b/app/models/blob_viewer/dependency_manager.rb
@@ -27,10 +27,17 @@ module BlobViewer
private
- def package_name_from_json(key)
- prepare!
+ def json_data
+ @json_data ||= begin
+ prepare!
+ JSON.parse(blob.data)
+ rescue
+ {}
+ end
+ end
- JSON.parse(blob.data)[key] rescue nil
+ def package_name_from_json(key)
+ json_data[key]
end
def package_name_from_method_call(name)
diff --git a/app/models/blob_viewer/package_json.rb b/app/models/blob_viewer/package_json.rb
index 09221efb56c..46cd2f04f4d 100644
--- a/app/models/blob_viewer/package_json.rb
+++ b/app/models/blob_viewer/package_json.rb
@@ -16,7 +16,25 @@ module BlobViewer
@package_name ||= package_name_from_json('name')
end
+ def package_type
+ private? ? 'private package' : super
+ end
+
def package_url
+ private? ? homepage : npm_url
+ end
+
+ private
+
+ def private?
+ !!json_data['private']
+ end
+
+ def homepage
+ json_data['homepage']
+ end
+
+ def npm_url
"https://www.npmjs.com/package/#{package_name}"
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 28f154581a9..d4690da3be6 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -287,8 +287,12 @@ module Ci
Ci::Pipeline.truncate_sha(sha)
end
+ # NOTE: This is loaded lazily and will never be nil, even if the commit
+ # cannot be found.
+ #
+ # Use constructs like: `pipeline.commit.present?`
def commit
- @commit ||= project.commit_by(oid: sha)
+ @commit ||= Commit.lazy(project, sha)
end
def branch?
@@ -338,12 +342,9 @@ module Ci
end
def latest?
- return false unless ref
-
- commit = project.commit(ref)
- return false unless commit
+ return false unless ref && commit.present?
- commit.sha == sha
+ project.commit(ref) == commit
end
def retried
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 13c31111134..2be07ca7d3c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -86,6 +86,20 @@ class Commit
def valid_hash?(key)
!!(/\A#{COMMIT_SHA_PATTERN}\z/ =~ key)
end
+
+ def lazy(project, oid)
+ BatchLoader.for({ project: project, oid: oid }).batch do |items, loader|
+ items_by_project = items.group_by { |i| i[:project] }
+
+ items_by_project.each do |project, commit_ids|
+ oids = commit_ids.map { |i| i[:oid] }
+
+ project.repository.commits_by(oids: oids).each do |commit|
+ loader.call({ project: commit.project, oid: commit.id }, commit) if commit
+ end
+ end
+ end
+ end
end
attr_accessor :raw
@@ -103,7 +117,7 @@ class Commit
end
def ==(other)
- (self.class === other) && (raw == other.raw)
+ other.is_a?(self.class) && raw == other.raw
end
def self.reference_prefix
@@ -224,8 +238,8 @@ class Commit
notes.includes(:author)
end
- def method_missing(m, *args, &block)
- @raw.__send__(m, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @raw.__send__(method, *args, &block) # rubocop:disable GitlabSecurity/PublicSend
end
def respond_to_missing?(method, include_private = false)
diff --git a/app/models/concerns/blocks_json_serialization.rb b/app/models/concerns/blocks_json_serialization.rb
new file mode 100644
index 00000000000..8019e6adc1c
--- /dev/null
+++ b/app/models/concerns/blocks_json_serialization.rb
@@ -0,0 +1,16 @@
+# Overrides `as_json` and `to_json` to raise an exception when called in order
+# to prevent accidentally exposing attributes
+#
+# Not that that would ever happen... but just in case.
+module BlocksJsonSerialization
+ extend ActiveSupport::Concern
+
+ JsonSerializationError = Class.new(StandardError)
+
+ def to_json(*)
+ raise JsonSerializationError,
+ "JSON serialization has been disabled on #{self.class.name}"
+ end
+
+ alias_method :as_json, :to_json
+end
diff --git a/app/models/concerns/time_trackable.rb b/app/models/concerns/time_trackable.rb
index 89fe6527647..5911b56c34c 100644
--- a/app/models/concerns/time_trackable.rb
+++ b/app/models/concerns/time_trackable.rb
@@ -24,7 +24,7 @@ module TimeTrackable
# rubocop:disable Gitlab/ModuleWithInstanceVariables
def spend_time(options)
@time_spent = options[:duration]
- @time_spent_user = options[:user]
+ @time_spent_user = User.find(options[:user_id])
@spent_at = options[:spent_at]
@original_total_time_spent = nil
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 99d99bc6deb..b3fa7d8176a 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -8,6 +8,8 @@ class Identity < ActiveRecord::Base
validates :extern_uid, allow_blank: true, uniqueness: { scope: :provider, case_sensitive: false }
validates :user_id, uniqueness: { scope: :provider }
+ before_save :ensure_normalized_extern_uid, if: :extern_uid_changed?
+
scope :with_provider, ->(provider) { where(provider: provider) }
scope :with_extern_uid, ->(provider, extern_uid) do
iwhere(extern_uid: normalize_uid(provider, extern_uid)).with_provider(provider)
@@ -24,4 +26,12 @@ class Identity < ActiveRecord::Base
uid.to_s
end
end
+
+ private
+
+ def ensure_normalized_extern_uid
+ return if extern_uid.nil?
+
+ self.extern_uid = Identity.normalize_uid(self.provider, self.extern_uid)
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 5183a216c53..3440c01b356 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1148,7 +1148,7 @@ class Project < ActiveRecord::Base
def change_head(branch)
if repository.branch_exists?(branch)
repository.before_change_head
- repository.write_ref('HEAD', "refs/heads/#{branch}", force: true)
+ repository.write_ref('HEAD', "refs/heads/#{branch}")
repository.copy_gitattributes(branch)
repository.after_change_head
reload_default_branch
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 4ec8ec9c8b2..a34f5e5439b 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -19,7 +19,6 @@ class Repository
attr_accessor :full_path, :disk_path, :project, :is_wiki
delegate :ref_name_for_sha, to: :raw_repository
- delegate :write_ref, to: :raw_repository
CreateTreeError = Class.new(StandardError)
@@ -118,6 +117,18 @@ class Repository
@commit_cache[oid] = find_commit(oid)
end
+ def commits_by(oids:)
+ return [] unless oids.present?
+
+ commits = Gitlab::Git::Commit.batch_by_oid(raw_repository, oids)
+
+ if commits.present?
+ Commit.decorate(commits, @project)
+ else
+ []
+ end
+ end
+
def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
options = {
repo: raw_repository,
@@ -244,10 +255,11 @@ class Repository
# This will still fail if the file is corrupted (e.g. 0 bytes)
begin
- write_ref(keep_around_ref_name(sha), sha, force: true)
- rescue Gitlab::Git::Repository::GitError => ex
- # Necessary because https://gitlab.com/gitlab-org/gitlab-ce/issues/20156
- return true if ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
+ write_ref(keep_around_ref_name(sha), sha)
+ rescue Rugged::ReferenceError => ex
+ Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
+ rescue Rugged::OSError => ex
+ raise unless ex.message =~ /Failed to create locked file/ && ex.message =~ /File exists/
Rails.logger.error "Unable to create #{REF_KEEP_AROUND} reference for repository #{path}: #{ex}"
end
@@ -257,6 +269,10 @@ class Repository
ref_exists?(keep_around_ref_name(sha))
end
+ def write_ref(ref_path, sha)
+ rugged.references.create(ref_path, sha, force: true)
+ end
+
def diverging_commit_counts(branch)
root_ref_hash = raw_repository.commit(root_ref).id
cache.fetch(:"diverging_commit_counts_#{branch.name}") do
@@ -1003,7 +1019,7 @@ class Repository
end
def create_ref(ref, ref_path)
- write_ref(ref_path, ref)
+ raw_repository.write_ref(ref_path, ref)
end
def ls_files(ref)
diff --git a/app/models/user.rb b/app/models/user.rb
index 51941f43919..b52f17cd6a8 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -18,6 +18,7 @@ class User < ActiveRecord::Base
include CreatedAtFilterable
include IgnorableColumn
include BulkMemberAccessLoad
+ include BlocksJsonSerialization
DEFAULT_NOTIFICATION_LEVEL = :participating
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index 38231f66009..8d4b9f14780 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -1,11 +1,14 @@
module Files
class BaseService < Commits::CreateService
+ FileChangedError = Class.new(StandardError)
+
def initialize(*args)
super
@author_email = params[:author_email]
@author_name = params[:author_name]
@commit_message = params[:commit_message]
+ @last_commit_sha = params[:last_commit_sha]
@file_path = params[:file_path]
@previous_path = params[:previous_path]
@@ -13,5 +16,16 @@ module Files
@file_content = params[:file_content]
@file_content = Base64.decode64(@file_content) if params[:file_content_encoding] == 'base64'
end
+
+ def file_has_changed?(path, commit_id)
+ return false unless commit_id
+
+ last_commit = Gitlab::Git::Commit
+ .last_for_path(@start_project.repository, @start_branch, path)
+
+ return false unless last_commit
+
+ last_commit.sha != commit_id
+ end
end
end
diff --git a/app/services/files/delete_service.rb b/app/services/files/delete_service.rb
index 7952e5c95d4..32a57484d4e 100644
--- a/app/services/files/delete_service.rb
+++ b/app/services/files/delete_service.rb
@@ -11,5 +11,15 @@ module Files
start_project: @start_project,
start_branch_name: @start_branch)
end
+
+ private
+
+ def validate!
+ super
+
+ if file_has_changed?(@file_path, @last_commit_sha)
+ raise FileChangedError, "You are attempting to delete a file that has been previously updated."
+ end
+ end
end
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
index bfacc462847..98a3e83c130 100644
--- a/app/services/files/multi_service.rb
+++ b/app/services/files/multi_service.rb
@@ -1,5 +1,7 @@
module Files
class MultiService < Files::BaseService
+ UPDATE_FILE_ACTIONS = %w(update move delete).freeze
+
def create_commit!
repository.multi_action(
user: current_user,
@@ -20,6 +22,7 @@ module Files
params[:actions].each do |action|
validate_action!(action)
+ validate_file_status!(action)
end
end
@@ -28,5 +31,15 @@ module Files
raise_error("Unknown action '#{action[:action]}'")
end
end
+
+ def validate_file_status!(action)
+ return unless UPDATE_FILE_ACTIONS.include?(action[:action])
+
+ file_path = action[:previous_path] || action[:file_path]
+
+ if file_has_changed?(file_path, action[:last_commit_id])
+ raise_error("The file has changed since you started editing it: #{file_path}")
+ end
+ end
end
end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index bcca1386bed..1902d1cea72 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -1,13 +1,5 @@
module Files
class UpdateService < Files::BaseService
- FileChangedError = Class.new(StandardError)
-
- def initialize(*args)
- super
-
- @last_commit_sha = params[:last_commit_sha]
- end
-
def create_commit!
repository.update_file(current_user, @file_path, @file_content,
message: @commit_message,
@@ -21,21 +13,10 @@ module Files
private
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
- def last_commit
- @last_commit ||= Gitlab::Git::Commit
- .last_for_path(@start_project.repository, @start_branch, @file_path)
- end
-
def validate!
super
- if file_has_changed?
+ if file_has_changed?(@file_path, @last_commit_sha)
raise FileChangedError, "You are attempting to update a file that has changed since you started editing it."
end
end
diff --git a/app/services/projects/unlink_fork_service.rb b/app/services/projects/unlink_fork_service.rb
index c499f384426..842fe4e09c4 100644
--- a/app/services/projects/unlink_fork_service.rb
+++ b/app/services/projects/unlink_fork_service.rb
@@ -5,7 +5,7 @@ module Projects
if fork_source = @project.fork_source
fork_source.lfs_objects.find_each do |lfs_object|
- lfs_object.projects << @project
+ lfs_object.projects << @project unless lfs_object.projects.include?(@project)
end
refresh_forks_count(fork_source)
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 06ac86cd5a9..669c1ba0a22 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -405,7 +405,7 @@ module QuickActions
if time_spent
@updates[:spend_time] = {
duration: time_spent,
- user: current_user,
+ user_id: current_user.id,
spent_at: time_spent_date
}
end
@@ -428,7 +428,7 @@ module QuickActions
current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :remove_time_spent do
- @updates[:spend_time] = { duration: :reset, user: current_user }
+ @updates[:spend_time] = { duration: :reset, user_id: current_user.id }
end
desc "Append the comment with #{SHRUG}"
diff --git a/app/views/admin/groups/index.html.haml b/app/views/admin/groups/index.html.haml
index 535251fef5e..25946ba6eaf 100644
--- a/app/views/admin/groups/index.html.haml
+++ b/app/views/admin/groups/index.html.haml
@@ -11,23 +11,7 @@
.search-field-holder
= search_field_tag :name, project_name, class: "form-control search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: 'Search by name'
= icon("search", class: "search-icon")
- .dropdown
- - toggle_text = @sort.present? ? sort_options_hash[@sort] : sort_title_recently_created
- = dropdown_toggle(toggle_text, { toggle: 'dropdown' })
- %ul.dropdown-menu.dropdown-menu-align-right
- %li.dropdown-header
- Sort by
- %li
- = link_to admin_groups_path(sort: sort_value_recently_created, name: project_name) do
- = sort_title_recently_created
- = link_to admin_groups_path(sort: sort_value_oldest_created, name: project_name) do
- = sort_title_oldest_created
- = link_to admin_groups_path(sort: sort_value_recently_updated, name: project_name) do
- = sort_title_recently_updated
- = link_to admin_groups_path(sort: sort_value_oldest_updated, name: project_name) do
- = sort_title_oldest_updated
- = link_to admin_groups_path(sort: sort_value_largest_group, name: project_name) do
- = sort_title_largest_group
+ = render "shared/groups/dropdown", options_hash: admin_groups_sort_options_hash
= link_to new_admin_group_path, class: "btn btn-new" do
New group
%ul.content-list
diff --git a/app/views/events/event/_push.html.haml b/app/views/events/event/_push.html.haml
index 9a763887b30..f85f5c5be88 100644
--- a/app/views/events/event/_push.html.haml
+++ b/app/views/events/event/_push.html.haml
@@ -7,7 +7,8 @@
%span.pushed #{event.action_name} #{event.ref_type}
%strong
- commits_link = project_commits_path(project, event.ref_name)
- = link_to_if project.repository.branch_exists?(event.ref_name), event.ref_name, commits_link, class: 'ref-name'
+ - should_link = event.tag? ? project.repository.tag_exists?(event.ref_name) : project.repository.branch_exists?(event.ref_name)
+ = link_to_if should_link, event.ref_name, commits_link, class: 'ref-name'
= render "events/event_scope", event: event
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 021de4f0caf..b8692009225 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -1,3 +1,5 @@
+= webpack_bundle_tag 'docs'
+
%div
- if current_application_settings.help_page_text.present?
= markdown_field(current_application_settings, :help_page_text)
@@ -37,8 +39,12 @@
Quick help
%ul.well-list
%li= link_to 'See our website for getting help', support_url
- %li= link_to 'Use the search bar on the top of this page', '#', onclick: 'Shortcuts.focusSearch(event)'
- %li= link_to 'Use shortcuts', '#', onclick: 'Shortcuts.toggleHelp()'
+ %li
+ %button.btn-blank.btn-link.js-trigger-search-bar{ type: 'button' }
+ Use the search bar on the top of this page
+ %li
+ %button.btn-blank.btn-link.js-trigger-shortcut{ type: 'button' }
+ Use shortcuts
- unless current_application_settings.help_page_hide_commercial_content?
%li= link_to 'Get a support subscription', 'https://about.gitlab.com/pricing/'
%li= link_to 'Compare GitLab editions', 'https://about.gitlab.com/features/#compare'
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
new file mode 100644
index 00000000000..8368e7a4563
--- /dev/null
+++ b/app/views/ide/index.html.haml
@@ -0,0 +1,12 @@
+- page_title 'IDE'
+
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag 'common_vue'
+ = webpack_bundle_tag 'ide'
+
+.ide-flash-container.flash-container
+
+#ide.ide-loading
+ .text-center
+ = icon('spinner spin 2x')
+ %h2.clgray= _('IDE Loading ...')
diff --git a/app/views/layouts/nav_only.html.haml b/app/views/layouts/nav_only.html.haml
new file mode 100644
index 00000000000..6fa4b39dc10
--- /dev/null
+++ b/app/views/layouts/nav_only.html.haml
@@ -0,0 +1,13 @@
+!!! 5
+%html{ lang: I18n.locale, class: page_class }
+ = render "layouts/head"
+ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page } }
+ = render 'peek/bar'
+ = render "layouts/header/default"
+ = render 'shared/outdated_browser'
+ .mobile-overlay
+ .alert-wrapper
+ = render "layouts/broadcast"
+ = yield :flash_message
+ = render "layouts/flash"
+ = yield
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 3a7a99462a6..79530e78154 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -7,7 +7,7 @@
.nav-block
= render 'projects/tree/tree_header', tree: @tree
- - if !show_new_repo? && commit
+ - if commit
= render 'shared/commit_well', commit: commit, ref: ref, project: project
= 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 c5e3a7945bd..8212ab9a31e 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -17,7 +17,7 @@
%a.js-md-preview-button{ href: "#md-preview-holder", tabindex: -1 }
Preview
- %li.md-header-toolbar
+ %li.md-header-toolbar.active
= markdown_toolbar_button({ icon: "bold", data: { "md-tag" => "**" }, title: "Add bold text" })
= markdown_toolbar_button({ icon: "italic", data: { "md-tag" => "*" }, title: "Add italic text" })
= markdown_toolbar_button({ icon: "quote", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 281363d2e01..2a77dedd9a2 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -12,6 +12,7 @@
.btn-group{ role: "group" }<
= edit_blob_link
+ = ide_blob_link
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index c4712bf3736..4d358052d43 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -6,21 +6,14 @@
- content_for :page_specific_javascripts do
= 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 }
- - 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
+ #tree-holder.tree-holder
+ = render 'blob', blob: @blob
- 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/_dependency_manager.html.haml b/app/views/projects/blob/viewers/_dependency_manager.html.haml
index a0f0215a5ff..87aa7c1dbf8 100644
--- a/app/views/projects/blob/viewers/_dependency_manager.html.haml
+++ b/app/views/projects/blob/viewers/_dependency_manager.html.haml
@@ -6,6 +6,6 @@
- if viewer.package_name
and defines a #{viewer.package_type} named
%strong<
- = link_to viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
+ = link_to_if viewer.package_url.present?, viewer.package_name, viewer.package_url, target: '_blank', rel: 'noopener noreferrer'
= link_to 'Learn more', viewer.manager_url, target: '_blank', rel: 'noopener noreferrer'
diff --git a/app/views/projects/clusters/gcp/_form.html.haml b/app/views/projects/clusters/gcp/_form.html.haml
index 0f6bae97571..e384b60d8d9 100644
--- a/app/views/projects/clusters/gcp/_form.html.haml
+++ b/app/views/projects/clusters/gcp/_form.html.haml
@@ -7,6 +7,9 @@
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :provider_gcp, @cluster.provider_gcp do |provider_gcp_field|
.form-group
diff --git a/app/views/projects/clusters/gcp/_show.html.haml b/app/views/projects/clusters/gcp/_show.html.haml
index 3fa9f69708a..bde85aed341 100644
--- a/app/views/projects/clusters/gcp/_show.html.haml
+++ b/app/views/projects/clusters/gcp/_show.html.haml
@@ -8,6 +8,11 @@
= form_for @cluster, url: namespace_project_cluster_path(@project.namespace, @project, @cluster), as: :cluster do |field|
= form_errors(@cluster)
+
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
diff --git a/app/views/projects/clusters/user/_form.html.haml b/app/views/projects/clusters/user/_form.html.haml
index 4a9bd5186c6..babfca0c567 100644
--- a/app/views/projects/clusters/user/_form.html.haml
+++ b/app/views/projects/clusters/user/_form.html.haml
@@ -3,6 +3,9 @@
.form-group
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
diff --git a/app/views/projects/clusters/user/_show.html.haml b/app/views/projects/clusters/user/_show.html.haml
index 5931e0b7f17..89595bca007 100644
--- a/app/views/projects/clusters/user/_show.html.haml
+++ b/app/views/projects/clusters/user/_show.html.haml
@@ -4,6 +4,10 @@
= field.label :name, s_('ClusterIntegration|Cluster name')
= field.text_field :name, class: 'form-control', placeholder: s_('ClusterIntegration|Cluster name')
+ .form-group
+ = field.label :environment_scope, s_('ClusterIntegration|Environment scope')
+ = field.text_field :environment_scope, class: 'form-control js-select-on-focus', readonly: !has_multiple_clusters?(@project), placeholder: s_('ClusterIntegration|Environment scope')
+
= field.fields_for :platform_kubernetes, @cluster.platform_kubernetes do |platform_kubernetes_field|
.form-group
= platform_kubernetes_field.label :api_url, s_('ClusterIntegration|API URL')
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index eab7879c7bf..1f28d8acff6 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -39,8 +39,6 @@
= icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- - if can_update_issue
- %li= link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'js-issuable-edit'
- unless current_user == @issue.author
%li= link_to 'Report abuse', new_abuse_report_path(user_id: @issue.author.id, ref_url: issue_url(@issue))
- if can_update_issue
@@ -52,9 +50,6 @@
%li.divider
%li= link_to 'New issue', new_project_issue_path(@project), title: 'New issue', id: 'new_issue_link'
- - if can_update_issue
- = link_to 'Edit', edit_project_issue_path(@project, @issue), class: 'hidden-xs hidden-sm btn btn-grouped js-issuable-edit'
-
= render 'shared/issuable/close_reopen_button', issuable: @issue, can_update: can_update_issue
- if can_report_spam
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 01ea9356af5..85946aec1f2 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,6 +1,6 @@
#js-pipeline-header-vue.pipeline-header-container
-- if @commit
+- if @commit.present?
.commit-box
%h3.commit-title
= markdown(@commit.title, pipeline: :single_line)
@@ -8,28 +8,28 @@
%pre.commit-description
= preserve(markdown(@commit.description, pipeline: :single_line))
-.info-well
- - if @commit.status
- .well-segment.pipeline-info
- .icon-container
- = icon('clock-o')
- = pluralize @pipeline.total_size, "job"
- - if @pipeline.ref
- from
- = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
- - if @pipeline.duration
- in
- = time_interval_in_words(@pipeline.duration)
- - if @pipeline.queued_duration
- = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
+ .info-well
+ - if @commit.status
+ .well-segment.pipeline-info
+ .icon-container
+ = icon('clock-o')
+ = pluralize @pipeline.total_size, "job"
+ - if @pipeline.ref
+ from
+ = link_to @pipeline.ref, project_ref_path(@project, @pipeline.ref), class: "ref-name"
+ - if @pipeline.duration
+ in
+ = time_interval_in_words(@pipeline.duration)
+ - if @pipeline.queued_duration
+ = "(queued for #{time_interval_in_words(@pipeline.queued_duration)})"
- .well-segment.branch-info
- .icon-container.commit-icon
- = custom_icon("icon_commit")
- = link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
- = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
- %span.text-expander
- \...
- %span.js-details-content.hide
- = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
- = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
+ .well-segment.branch-info
+ .icon-container.commit-icon
+ = custom_icon("icon_commit")
+ = link_to @commit.short_id, project_commit_path(@project, @pipeline.sha), class: "commit-sha js-details-short"
+ = link_to("#", class: "js-details-expand hidden-xs hidden-sm") do
+ %span.text-expander
+ \...
+ %span.js-details-content.hide
+ = link_to @pipeline.sha, project_commit_path(@project, @pipeline.sha), class: "commit-sha commit-hash-full"
+ = clipboard_button(text: @pipeline.sha, title: "Copy commit SHA to clipboard")
diff --git a/app/views/projects/tree/_old_tree_content.html.haml b/app/views/projects/tree/_old_tree_content.html.haml
deleted file mode 100644
index 6ea78851b8d..00000000000
--- a/app/views/projects/tree/_old_tree_content.html.haml
+++ /dev/null
@@ -1,24 +0,0 @@
-.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
deleted file mode 100644
index 7f636b7e0e8..00000000000
--- a/app/views/projects/tree/_old_tree_header.html.haml
+++ /dev/null
@@ -1,64 +0,0 @@
-- if on_top_of_branch?
- - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
-- else
- - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
-
-%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
- %a.btn.add-to-tree{ addtotree_toggle_attributes }
- = sprite_icon('plus', size: 16, css_class: 'pull-left')
- = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
- - if on_top_of_branch?
- .add-to-tree-dropdown
- %ul.dropdown-menu
- - if can_edit_tree?
- %li
- = link_to project_new_blob_path(@project, @id) do
- #{ _('New file') }
- %li
- = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
- #{ _('Upload file') }
- %li
- = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
- #{ _('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
- #{ _('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
- #{ _('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
- #{ _('New directory') }
-
- %li.divider
- %li
- = link_to new_project_branch_path(@project) do
- #{ _('New branch') }
- %li
- = link_to new_project_tag_path(@project) do
- #{ _('New tag') }
diff --git a/app/views/projects/tree/_tree_content.html.haml b/app/views/projects/tree/_tree_content.html.haml
index a4bdd67209d..6ea78851b8d 100644
--- a/app/views/projects/tree/_tree_content.html.haml
+++ b/app/views/projects/tree/_tree_content.html.haml
@@ -1,5 +1,24 @@
-- 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
+.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/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index c02f7ee37ed..d1ecef39475 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -2,16 +2,78 @@
.tree-ref-holder
= render 'shared/ref_switcher', destination: 'tree', path: @path, show_create: true
- - if show_new_repo? && can_push_branch?(@project, @ref)
- .js-new-dropdown
- - else
- = render 'projects/tree/old_tree_header'
+ - if on_top_of_branch?
+ - addtotree_toggle_attributes = { href: '#', 'data-toggle': 'dropdown', 'data-target': '.add-to-tree-dropdown' }
+ - else
+ - addtotree_toggle_attributes = { title: _("You can only add files when you are on a branch"), data: { container: 'body' }, class: 'disabled has-tooltip' }
+
+ %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
+ %a.btn.add-to-tree{ addtotree_toggle_attributes }
+ = sprite_icon('plus', size: 16, css_class: 'pull-left')
+ = sprite_icon('arrow-down', size: 16, css_class: 'pull-left')
+ - if on_top_of_branch?
+ .add-to-tree-dropdown
+ %ul.dropdown-menu
+ - if can_edit_tree?
+ %li
+ = link_to project_new_blob_path(@project, @id) do
+ #{ _('New file') }
+ %li
+ = link_to '#modal-upload-blob', { 'data-target' => '#modal-upload-blob', 'data-toggle' => 'modal' } do
+ #{ _('Upload file') }
+ %li
+ = link_to '#modal-create-new-dir', { 'data-target' => '#modal-create-new-dir', 'data-toggle' => 'modal' } do
+ #{ _('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
+ #{ _('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
+ #{ _('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
+ #{ _('New directory') }
+
+ %li.divider
+ %li
+ = link_to new_project_branch_path(@project) do
+ #{ _('New branch') }
+ %li
+ = link_to new_project_tag_path(@project) do
+ #{ _('New tag') }
.tree-controls
- - if show_new_repo?
- .editable-mode
- - else
- = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
+ - if show_new_ide?
+ = succeed " " do
+ = link_to ide_edit_path(@project, @id), class: 'btn btn-default' do
+ = ide_edit_text
+
+ = link_to s_('Commits|History'), project_commits_path(@project, @id), class: 'btn'
= render 'projects/find_file_link'
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 64cc70053ef..3b4057e56d0 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -6,11 +6,6 @@
= 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'
-
-%div{ class: [(container_class unless show_new_repo?), ("limit-container-width" unless fluid_layout)] }
+%div{ class: [(container_class), ("limit-container-width" unless fluid_layout)] }
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index f4a4bfaec54..479bd2cdb38 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,6 +1,6 @@
- show_create = local_assigns.fetch(:show_create, false)
-- show_new_branch_form = show_new_repo? && show_create && can?(current_user, :push_code, @project)
+- show_new_branch_form = show_new_ide? && show_create && can?(current_user, :push_code, @project)
- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_project_refs_path(@project), method: :get, class: "project-refs-form" do
= hidden_field_tag :destination, destination
diff --git a/app/views/shared/boards/components/_sidebar.html.haml b/app/views/shared/boards/components/_sidebar.html.haml
index b3f73e96b81..8e5e32e9f16 100644
--- a/app/views/shared/boards/components/_sidebar.html.haml
+++ b/app/views/shared/boards/components/_sidebar.html.haml
@@ -1,5 +1,4 @@
-%board-sidebar{ "inline-template" => true,
- ":current-user" => "#{current_user ? current_user.to_json(only: [:username, :id, :name], methods: [:avatar_url]) : {}}" }
+%board-sidebar{ "inline-template" => true, ":current-user" => (UserSerializer.new.represent(current_user) || {}).to_json }
%transition{ name: "boards-sidebar-slide" }
%aside.right-sidebar.right-sidebar-expanded.issue-boards-sidebar{ "v-show" => "showSidebar" }
.issuable-sidebar
diff --git a/app/views/shared/groups/_dropdown.html.haml b/app/views/shared/groups/_dropdown.html.haml
index 8e6747ca740..1a259b679c7 100644
--- a/app/views/shared/groups/_dropdown.html.haml
+++ b/app/views/shared/groups/_dropdown.html.haml
@@ -1,3 +1,4 @@
+- options_hash = local_assigns.fetch(:options_hash, groups_sort_options_hash)
- show_archive_options = local_assigns.fetch(:show_archive_options, false)
- if @sort.present?
- default_sort_by = @sort
@@ -10,12 +11,12 @@
.dropdown.inline.js-group-filter-dropdown-wrap.append-right-10
%button.dropdown-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
%span.dropdown-label
- = sort_options_hash[default_sort_by]
+ = options_hash[default_sort_by]
= icon('chevron-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
%li.dropdown-header
= _("Sort by")
- - groups_sort_options_hash.each do |value, title|
+ - options_hash.each do |value, title|
%li.js-filter-sort-order
= link_to filter_groups_path(sort: value), class: ("is-active" if default_sort_by == value) do
= title
diff --git a/app/views/shared/repo/_repo.html.haml b/app/views/shared/repo/_repo.html.haml
deleted file mode 100644
index 87e8c416194..00000000000
--- a/app/views/shared/repo/_repo.html.haml
+++ /dev/null
@@ -1,13 +0,0 @@
-- @no_container = true;
-#repo{ data: { root: @path.empty?.to_s,
- root_url: project_tree_path(project),
- url: content_url,
- current_branch: @ref,
- ref: @commit.id,
- project_name: project.name,
- project_url: project_path(project),
- project_id: project.id,
- new_merge_request_url: namespace_project_new_merge_request_path(project.namespace, project, merge_request: { source_branch: '' }),
- can_commit: (!!can_push_branch?(project, @ref)).to_s,
- on_top_of_branch: (!!on_top_of_branch?(project, @ref)).to_s,
- current_path: @path } }
diff --git a/app/workers/concerns/project_import_options.rb b/app/workers/concerns/project_import_options.rb
new file mode 100644
index 00000000000..10b971344f7
--- /dev/null
+++ b/app/workers/concerns/project_import_options.rb
@@ -0,0 +1,23 @@
+module ProjectImportOptions
+ extend ActiveSupport::Concern
+
+ included do
+ IMPORT_RETRY_COUNT = 5
+
+ sidekiq_options retry: IMPORT_RETRY_COUNT, status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+
+ # We only want to mark the project as failed once we exhausted all retries
+ sidekiq_retries_exhausted do |job|
+ project = Project.find(job['args'].first)
+
+ action = if project.forked?
+ "fork"
+ else
+ "import"
+ end
+
+ project.mark_import_as_failed("Every #{action} attempt has failed: #{job['error_message']}. Please try again.")
+ Sidekiq.logger.warn "Failed #{job['class']} with #{job['args']}: #{job['error_message']}"
+ end
+ end
+end
diff --git a/app/workers/concerns/project_start_import.rb b/app/workers/concerns/project_start_import.rb
index 0704ebbb0fd..4e55a1ee3d6 100644
--- a/app/workers/concerns/project_start_import.rb
+++ b/app/workers/concerns/project_start_import.rb
@@ -1,3 +1,4 @@
+# Used in EE by mirroring
module ProjectStartImport
def start(project)
if project.import_started? && project.import_jid == self.jid
diff --git a/app/workers/expire_pipeline_cache_worker.rb b/app/workers/expire_pipeline_cache_worker.rb
index 3e34de22c19..db73d37868a 100644
--- a/app/workers/expire_pipeline_cache_worker.rb
+++ b/app/workers/expire_pipeline_cache_worker.rb
@@ -13,7 +13,7 @@ class ExpirePipelineCacheWorker
store.touch(project_pipelines_path(project))
store.touch(project_pipeline_path(project, pipeline))
- store.touch(commit_pipelines_path(project, pipeline.commit)) if pipeline.commit
+ store.touch(commit_pipelines_path(project, pipeline.commit)) unless pipeline.commit.nil?
store.touch(new_merge_request_pipelines_path(project))
each_pipelines_merge_request_path(project, pipeline) do |path|
store.touch(path)
diff --git a/app/workers/repository_fork_worker.rb b/app/workers/repository_fork_worker.rb
index a07ef1705a1..d1c57b82681 100644
--- a/app/workers/repository_fork_worker.rb
+++ b/app/workers/repository_fork_worker.rb
@@ -1,11 +1,8 @@
class RepositoryForkWorker
- ForkError = Class.new(StandardError)
-
include ApplicationWorker
include Gitlab::ShellAdapter
include ProjectStartImport
-
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ include ProjectImportOptions
def perform(project_id, forked_from_repository_storage_path, source_disk_path)
project = Project.find(project_id)
@@ -18,20 +15,12 @@ class RepositoryForkWorker
result = gitlab_shell.fork_repository(forked_from_repository_storage_path, source_disk_path,
project.repository_storage_path, project.disk_path)
- raise ForkError, "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
+ raise "Unable to fork project #{project_id} for repository #{source_disk_path} -> #{project.disk_path}" unless result
project.repository.after_import
- raise ForkError, "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
+ raise "Project #{project_id} had an invalid repository after fork" unless project.valid_repo?
project.import_finish
- rescue ForkError => ex
- fail_fork(project, ex.message)
- raise
- rescue => ex
- return unless project
-
- fail_fork(project, ex.message)
- raise ForkError, "#{ex.class} #{ex.message}"
end
private
@@ -42,9 +31,4 @@ class RepositoryForkWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while forking.")
false
end
-
- def fail_fork(project, message)
- Rails.logger.error(message)
- project.mark_import_as_failed(message)
- end
end
diff --git a/app/workers/repository_import_worker.rb b/app/workers/repository_import_worker.rb
index 55715c83cb1..31e2798c36b 100644
--- a/app/workers/repository_import_worker.rb
+++ b/app/workers/repository_import_worker.rb
@@ -1,11 +1,8 @@
class RepositoryImportWorker
- ImportError = Class.new(StandardError)
-
include ApplicationWorker
include ExceptionBacktrace
include ProjectStartImport
-
- sidekiq_options status_expiration: StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION
+ include ProjectImportOptions
def perform(project_id)
project = Project.find(project_id)
@@ -23,17 +20,9 @@ class RepositoryImportWorker
# to those importers to mark the import process as complete.
return if service.async?
- raise ImportError, result[:message] if result[:status] == :error
+ raise result[:message] if result[:status] == :error
project.after_import
- rescue ImportError => ex
- fail_import(project, ex.message)
- raise
- rescue => ex
- return unless project
-
- fail_import(project, ex.message)
- raise ImportError, "#{ex.class} #{ex.message}"
end
private
@@ -44,8 +33,4 @@ class RepositoryImportWorker
Rails.logger.info("Project #{project.full_path} was in inconsistent state (#{project.import_status}) while importing.")
false
end
-
- def fail_import(project, message)
- project.mark_import_as_failed(message)
- end
end
diff --git a/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml b/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml
new file mode 100644
index 00000000000..db2bd6e692b
--- /dev/null
+++ b/changelogs/unreleased/15922-validate-file-status-when-commiting-multiple-files.yml
@@ -0,0 +1,5 @@
+---
+title: 'Validate file status when commiting multiple files'
+merge_request: 15922
+author:
+type: added
diff --git a/changelogs/unreleased/33028-event-tag-links.yml b/changelogs/unreleased/33028-event-tag-links.yml
new file mode 100644
index 00000000000..1d674200dcd
--- /dev/null
+++ b/changelogs/unreleased/33028-event-tag-links.yml
@@ -0,0 +1,5 @@
+---
+title: Fix tags in the Activity tab not being clickable
+merge_request: 15996
+author: Mario de la Ossa
+type: fixed
diff --git a/changelogs/unreleased/36020-private-npm-modules.yml b/changelogs/unreleased/36020-private-npm-modules.yml
new file mode 100644
index 00000000000..5c2585a602e
--- /dev/null
+++ b/changelogs/unreleased/36020-private-npm-modules.yml
@@ -0,0 +1,5 @@
+---
+title: Do not generate NPM links for private NPM modules in blob view
+merge_request: 16002
+author: Mario de la Ossa
+type: added
diff --git a/changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml b/changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml
new file mode 100644
index 00000000000..8348e3e8ceb
--- /dev/null
+++ b/changelogs/unreleased/36958-enable-ordering-projects-subgroups-by-name.yml
@@ -0,0 +1,5 @@
+---
+title: Enable ordering of groups and their children by name
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/38318-search-merge-requests-with-api.yml b/changelogs/unreleased/38318-search-merge-requests-with-api.yml
new file mode 100644
index 00000000000..d8b2f1f25c8
--- /dev/null
+++ b/changelogs/unreleased/38318-search-merge-requests-with-api.yml
@@ -0,0 +1,5 @@
+---
+title: Add optional search param for Merge Requests API
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml b/changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml
new file mode 100644
index 00000000000..ce238a2c79f
--- /dev/null
+++ b/changelogs/unreleased/39246-fork-and-import-jobs-should-only-be-marked-as-failed-when-the-number-of-retries-was-exhausted.yml
@@ -0,0 +1,5 @@
+---
+title: Only mark import and fork jobs as failed once all Sidekiq retries get exhausted
+merge_request: 15844
+author:
+type: changed
diff --git a/changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml b/changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml
new file mode 100644
index 00000000000..e2fade2bfd9
--- /dev/null
+++ b/changelogs/unreleased/40040-decouple-multi-file-editor-from-file-list.yml
@@ -0,0 +1,5 @@
+---
+title: Adds the multi file editor as a new beta feature
+merge_request: 15430
+author:
+type: feature
diff --git a/changelogs/unreleased/40063-markdown-editor-improvements.yml b/changelogs/unreleased/40063-markdown-editor-improvements.yml
new file mode 100644
index 00000000000..fa2f09408b4
--- /dev/null
+++ b/changelogs/unreleased/40063-markdown-editor-improvements.yml
@@ -0,0 +1,5 @@
+---
+title: Hide markdown toolbar in preview mode
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/bvl-fix-unlinking-with-lfs-objects.yml b/changelogs/unreleased/bvl-fix-unlinking-with-lfs-objects.yml
new file mode 100644
index 00000000000..058d686e74c
--- /dev/null
+++ b/changelogs/unreleased/bvl-fix-unlinking-with-lfs-objects.yml
@@ -0,0 +1,6 @@
+---
+title: Don't link LFS objects to a project when unlinking forks when they were already
+ linked
+merge_request: 16006
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-docs-help-shortcut.yml b/changelogs/unreleased/fix-docs-help-shortcut.yml
new file mode 100644
index 00000000000..8c172e44160
--- /dev/null
+++ b/changelogs/unreleased/fix-docs-help-shortcut.yml
@@ -0,0 +1,5 @@
+---
+title: Fix shortcut links on help page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-onion-skin-reenter.yml b/changelogs/unreleased/fix-onion-skin-reenter.yml
new file mode 100644
index 00000000000..66b12c037b0
--- /dev/null
+++ b/changelogs/unreleased/fix-onion-skin-reenter.yml
@@ -0,0 +1,5 @@
+---
+title: Fix onion-skin re-entering state
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml b/changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml
new file mode 100644
index 00000000000..3fd8b0eb988
--- /dev/null
+++ b/changelogs/unreleased/fj-40279-normalize-ldap-dn-api.yml
@@ -0,0 +1,5 @@
+---
+title: Normalizing Identity extern_uid when saving the record
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/remove-links-mr-empty-state.yml b/changelogs/unreleased/remove-links-mr-empty-state.yml
new file mode 100644
index 00000000000..c666bc2c81d
--- /dev/null
+++ b/changelogs/unreleased/remove-links-mr-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Remove related links in MR widget when empty state
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/show-inline-edit-btn.yml b/changelogs/unreleased/show-inline-edit-btn.yml
new file mode 100644
index 00000000000..8cfe9b7d75a
--- /dev/null
+++ b/changelogs/unreleased/show-inline-edit-btn.yml
@@ -0,0 +1,5 @@
+---
+title: Move edit button to second row on issue page (and change it to a pencil icon)
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-empty-repo-importer.yml b/changelogs/unreleased/zj-empty-repo-importer.yml
new file mode 100644
index 00000000000..71d50af9a04
--- /dev/null
+++ b/changelogs/unreleased/zj-empty-repo-importer.yml
@@ -0,0 +1,5 @@
+---
+title: Fix GitHub importer using removed interface
+merge_request:
+author:
+type: fixed
diff --git a/config/routes.rb b/config/routes.rb
index 016140e0ede..f162043dd5e 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,6 +43,8 @@ Rails.application.routes.draw do
get 'liveness' => 'health#liveness'
get 'readiness' => 'health#readiness'
post 'storage_check' => 'health#storage_check'
+ get 'ide' => 'ide#index'
+ get 'ide/*vueroute' => 'ide#index', format: false
resources :metrics, only: [:index]
mount Peek::Railtie => '/peek'
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 0cb69141a73..f7063442618 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -32,10 +32,10 @@ var config = {
boards: './boards/boards_bundle.js',
common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js',
- common_d3: ['d3'],
cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
commit_pipelines: './commit/pipelines/pipelines_bundle.js',
deploy_keys: './deploy_keys/index.js',
+ docs: './docs/docs_bundle.js',
diff_notes: './diff_notes/diff_notes_bundle.js',
environments: './environments/environments_bundle.js',
environments_folder: './environments/folder/environments_folder_bundle.js',
@@ -70,7 +70,7 @@ var config = {
protected_branches: './protected_branches',
protected_tags: './protected_tags',
registry_list: './registry/index.js',
- repo: './repo/index.js',
+ ide: './ide/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',
@@ -209,7 +209,7 @@ var config = {
'pipelines',
'pipelines_details',
'registry_list',
- 'repo',
+ 'ide',
'schedule_form',
'schedules_index',
'sidebar',
@@ -229,6 +229,9 @@ var config = {
'monitoring',
'users',
],
+ minChunks: function (module, count) {
+ return module.resource && /d3-/.test(module.resource);
+ },
}),
// create cacheable common library bundles
diff --git a/db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb b/db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb
new file mode 100644
index 00000000000..286721a0894
--- /dev/null
+++ b/db/post_migrate/20171219121201_normalize_extern_uid_from_identities.rb
@@ -0,0 +1,29 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class NormalizeExternUidFromIdentities < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'NormalizeLdapExternUidsRange'.freeze
+ DELAY_INTERVAL = 10.seconds
+
+ disable_ddl_transaction!
+
+ class Identity < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'identities'
+ end
+
+ def up
+ ldap_identities = Identity.where("provider like 'ldap%'")
+
+ if ldap_identities.any?
+ queue_background_migration_jobs_by_range_at_intervals(Identity, MIGRATION, DELAY_INTERVAL)
+ end
+ end
+
+ def down
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 2048c50f892..2b20628bd53 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: 20171213160445) do
+ActiveRecord::Schema.define(version: 20171219121201) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 42666357faf..b85a166089d 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -1,6 +1,6 @@
# Configuring GitLab for HA
-Assuming you have already configured a database, Redis, and NFS, you can
+Assuming you have already configured a [database](database.md), [Redis](redis.md), and [NFS](nfs.md), you can
configure the GitLab application server(s) now. Complete the steps below
for each GitLab application server in your environment.
@@ -48,34 +48,33 @@ for each GitLab application server in your environment.
data locations. See [NFS documentation](nfs.md) for `/etc/gitlab/gitlab.rb`
configuration values for various scenarios. The example below assumes you've
added NFS mounts in the default data locations.
-
+
```ruby
external_url 'https://gitlab.example.com'
# Prevent GitLab from starting if NFS data mounts are not available
high_availability['mountpoint'] = '/var/opt/gitlab/git-data'
-
+
# Disable components that will not be on the GitLab application server
- postgresql['enable'] = false
- redis['enable'] = false
-
+ roles ['application_role']
+
# PostgreSQL connection details
gitlab_rails['db_adapter'] = 'postgresql'
gitlab_rails['db_encoding'] = 'unicode'
gitlab_rails['db_host'] = '10.1.0.5' # IP/hostname of database server
gitlab_rails['db_password'] = 'DB password'
-
+
# Redis connection details
gitlab_rails['redis_port'] = '6379'
gitlab_rails['redis_host'] = '10.1.0.6' # IP/hostname of Redis server
gitlab_rails['redis_password'] = 'Redis Password'
```
-
- > **Note:** To maintain uniformity of links across HA clusters, the `external_url`
- on the first application server as well as the additional application
- servers should point to the external url that users will use to access GitLab.
+
+ > **Note:** To maintain uniformity of links across HA clusters, the `external_url`
+ on the first application server as well as the additional application
+ servers should point to the external url that users will use to access GitLab.
In a typical HA setup, this will be the url of the load balancer which will
- route traffic to all GitLab application servers in the HA cluster.
+ route traffic to all GitLab application servers in the HA cluster.
1. Run `sudo gitlab-ctl reconfigure` to compile the configuration.
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 5a4a8d888b3..c9b72d4a1dd 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -84,6 +84,7 @@ POST /projects/:id/repository/commits
| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
| `encoding` | string | no | `text` or `base64`. `text` is default. |
+| `last_commit_id` | string | no | Last known file commit id. Will be only considered in update, move and delete actions. |
```bash
PAYLOAD=$(cat << 'JSON'
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 880b0ed2c65..4d3592e8f71 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -47,6 +47,7 @@ Parameters:
| `author_id` | integer | no | Returns merge requests created by the given user `id`. Combine with `scope=all` or `scope=assigned-to-me` |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `search` | string | no | Search merge requests against their `title` and `description` |
```json
[
@@ -161,6 +162,7 @@ Parameters:
| `author_id` | integer | no | Returns merge requests created by the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `assignee_id` | integer | no | Returns merge requests assigned to the given user `id` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `my_reaction_emoji` | string | no | Return merge requests reacted by the authenticated user by the given `emoji` _([Introduced][ce-14016] in GitLab 10.0)_ |
+| `search` | string | no | Search merge requests against their `title` and `description` |
```json
[
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index c517a38a8ba..a1a0b1b756c 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -151,3 +151,4 @@ Parameters:
- `author_email` (optional) - Specify the commit author's email address
- `author_name` (optional) - Specify the commit author's name
- `commit_message` (required) - Commit message
+- `last_commit_id` (optional) - Last known file commit id
diff --git a/doc/ci/docker/using_docker_build.md b/doc/ci/docker/using_docker_build.md
index 0a2419b7ed2..22afcb9199d 100644
--- a/doc/ci/docker/using_docker_build.md
+++ b/doc/ci/docker/using_docker_build.md
@@ -164,6 +164,21 @@ not without its own challenges:
- By default, `docker:dind` uses `--storage-driver vfs` which is the slowest
form offered. To use a different driver, see
[Using the overlayfs driver](#using-the-overlayfs-driver).
+- Since the `docker:dind` container and the runner container don't share their
+ root filesystem, the job's working directory can be used as a mount point for
+ children containers. For example, if you have files you want to share with a
+ child container, you may create a subdirectory under `/builds/$CI_PROJECT_PATH`
+ and use it as your mount point (for a more thorough explanation, check [issue
+ #41227](https://gitlab.com/gitlab-org/gitlab-ce/issues/41227)):
+
+ ```yaml
+ variables:
+ MOUNT_POINT: /builds/$CI_PROJECT_PATH/mnt
+
+ script:
+ - mkdir -p "$MOUNT_POINT"
+ - docker run -v "$MOUNT_POINT:/mnt" my-docker-image
+ ```
An example project using this approach can be found here: https://gitlab.com/gitlab-examples/docker.
diff --git a/doc/ci/examples/browser_performance.md b/doc/ci/examples/browser_performance.md
new file mode 100644
index 00000000000..a7945d05cd0
--- /dev/null
+++ b/doc/ci/examples/browser_performance.md
@@ -0,0 +1,50 @@
+# Browser Performance Testing with the Sitespeed.io container
+
+This example shows how to run the [Sitespeed.io container](https://hub.docker.com/r/sitespeedio/sitespeed.io/) on your code by using
+GitLab CI/CD and [Sitespeed.io](https://www.sitespeed.io) using Docker-in-Docker.
+
+First, you need a GitLab Runner with the [docker-in-docker executor](../docker/using_docker_build.md#use-docker-in-docker-executor).
+
+Once you set up the Runner, add a new job to `.gitlab-ci.yml`, called `performance`:
+
+```yaml
+ stage: performance
+ image: docker:git
+ services:
+ - docker:dind
+ script:
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io --outputFolder sitespeed-results https://my.website.com
+ artifacts:
+ paths:
+ - sitespeed-results/
+```
+
+This will create a `performance` job in your CI/CD pipeline and will run Sitespeed.io against the webpage you define. The full HTML Sitespeed.io report will be saved as an artifact, and if you have Pages enabled it can be viewed directly in your browser. For further customization options of Sitespeed.io, including the ability to provide a list of URLs to test, please consult their [documentation](https://www.sitespeed.io/documentation/sitespeed.io/configuration/).
+
+For GitLab [Enterprise Edition Premium](https://about.gitlab.com/gitlab-ee/) users, a performance score can be automatically
+extracted and shown right in the merge request widget. Learn more about [Browser Performance Testing](https://docs.gitlab.com/ee/user/project/merge_requests/browser_performance_testing.html).
+
+## Performance testing on Review Apps
+
+The above CI YML is great for testing against static environments, and it can be extended for dynamic environments. There are a few extra steps to take to set this up:
+1. The `performance` job should run after the environment has started.
+1. In the `deploy` job, persist the hostname so it is available to the `performance` job. The same can be done for static environments like staging and production to unify the code path. Saving it as an artifact is as simple as `echo $CI_ENVIRONMENT_URL > environment_url.txt`.
+1. In the `performance` job read the artifact into an environment variable, like `$CI_ENVIRONMENT_URL`, and use it to parameterize the test URL's.
+1. Now you can run the Sitespeed.io container against the desired hostname and paths.
+
+A simple `performance` job would look like:
+
+```yaml
+ stage: performance
+ image: docker:git
+ services:
+ - docker:dind
+ script:
+ - export CI_ENVIRONMENT_URL=$(cat environment_url.txt)
+ - mkdir sitespeed-results
+ - docker run --shm-size=1g --rm -v "$(pwd)":/sitespeed.io sitespeedio/sitespeed.io --outputFolder sitespeed-results $CI_ENVIRONMENT_URL
+ artifacts:
+ paths:
+ - sitespeed-results/
+```
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index db13e0e6249..9cb1f708a6a 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -18,6 +18,10 @@ like `docs.gitlab.com/user/project/merge_requests.html`. With this pattern,
you can immediately tell that you are navigating a user related documentation
and is about the project and its merge requests.
+Do not create summaries of similar types of content (e.g. an index of all articles, videos, etc.),
+rather organise content by its subject (e.g. everything related to CI goes together)
+and cross-link between any related content.
+
The table below shows what kind of documentation goes where.
| Directory | What belongs here |
diff --git a/doc/development/fe_guide/axios.md b/doc/development/fe_guide/axios.md
index 962fe3dcec9..1daa6758171 100644
--- a/doc/development/fe_guide/axios.md
+++ b/doc/development/fe_guide/axios.md
@@ -11,7 +11,7 @@ This exported module should be used instead of directly using `axios` to ensure
## Usage
```javascript
- import axios from '~/lib/utils/axios_utils';
+ import axios from './lib/utils/axios_utils';
axios.get(url)
.then((response) => {
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 6e04f09f322..93b057430d3 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -278,17 +278,6 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content('Your changes could not be committed')
end
- step 'I create bare repo' do
- click_link 'Create empty bare repository'
- end
-
- step 'I click on "README" link' do
- click_link 'README'
-
- # Remove pre-receive hook so we can push without auth
- FileUtils.rm_f(File.join(@project.repository.path, 'hooks', 'pre-receive'))
- end
-
step "I switch ref to 'test'" do
first('.js-project-refs-dropdown').click
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 5f943ba27d1..b29c5848aef 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -8,7 +8,7 @@ module API
helpers do
def find_issues(args = {})
- args = params.merge(args)
+ args = declared_params.merge(args)
args.delete(:id)
args[:milestone_title] = args.delete(:milestone)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index d34886fca2e..02f2b75ab9d 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -8,7 +8,7 @@ module API
helpers do
def find_merge_requests(args = {})
- args = params.merge(args)
+ args = declared_params.merge(args)
args[:milestone_title] = args.delete(:milestone)
args[:label_name] = args.delete(:labels)
@@ -41,6 +41,7 @@ module API
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
desc: 'Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`'
optional :my_reaction_emoji, type: String, desc: 'Return issues reacted by the authenticated user by the given emoji'
+ optional :search, type: String, desc: 'Search merge requests for text present in the title or description'
use :pagination
end
end
diff --git a/lib/api/time_tracking_endpoints.rb b/lib/api/time_tracking_endpoints.rb
index df4632346dd..2bb451dea89 100644
--- a/lib/api/time_tracking_endpoints.rb
+++ b/lib/api/time_tracking_endpoints.rb
@@ -85,7 +85,7 @@ module API
update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
- user: current_user
+ user_id: current_user.id
})
end
@@ -97,7 +97,7 @@ module API
authorize! update_issuable_key, load_issuable
status :ok
- update_issuable(spend_time: { duration: :reset, user: current_user })
+ update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
end
desc "Show time stats for a project #{issuable_name}"
diff --git a/lib/api/v3/time_tracking_endpoints.rb b/lib/api/v3/time_tracking_endpoints.rb
index d5b90e435ba..1aad39815f9 100644
--- a/lib/api/v3/time_tracking_endpoints.rb
+++ b/lib/api/v3/time_tracking_endpoints.rb
@@ -86,7 +86,7 @@ module API
update_issuable(spend_time: {
duration: Gitlab::TimeTrackingFormatter.parse(params.delete(:duration)),
- user: current_user
+ user_id: current_user.id
})
end
@@ -98,7 +98,7 @@ module API
authorize! update_issuable_key, load_issuable
status :ok
- update_issuable(spend_time: { duration: :reset, user: current_user })
+ update_issuable(spend_time: { duration: :reset, user_id: current_user.id })
end
desc "Show time stats for a project #{issuable_name}"
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index e90b158fb34..145721dea76 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -228,6 +228,19 @@ module Gitlab
end
end
end
+
+ # Only to be used when the object ids will not necessarily have a
+ # relation to each other. The last 10 commits for a branch for example,
+ # should go through .where
+ def batch_by_oid(repo, oids)
+ repo.gitaly_migrate(:list_commits_by_oid) do |is_enabled|
+ if is_enabled
+ repo.gitaly_commit_client.list_commits_by_oid(oids)
+ else
+ oids.map { |oid| find(repo, oid) }.compact
+ end
+ end
+ end
end
def initialize(repository, raw_commit, head = nil)
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 848a782446a..044c60caa05 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -1101,17 +1101,12 @@ module Gitlab
end
end
- def write_ref(ref_path, ref, force: false)
+ def write_ref(ref_path, ref)
raise ArgumentError, "invalid ref_path #{ref_path.inspect}" if ref_path.include?(' ')
raise ArgumentError, "invalid ref #{ref.inspect}" if ref.include?("\x00")
- ref = "refs/heads/#{ref}" unless ref.start_with?("refs") || ref =~ /\A[a-f0-9]+\z/i
-
- rugged.references.create(ref_path, ref, force: force)
- rescue Rugged::ReferenceError => ex
- raise GitError, "could not create ref #{ref_path}: #{ex}"
- rescue Rugged::OSError => ex
- raise GitError, "could not create ref #{ref_path}: #{ex}"
+ input = "update #{ref_path}\x00#{ref}\x00\x00"
+ run_git!(%w[update-ref --stdin -z]) { |stdin| stdin.write(input) }
end
def fetch_ref(source_repository, source_ref:, target_ref:)
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 7985f5b5457..fb3e27770b4 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -169,6 +169,15 @@ module Gitlab
consume_commits_response(response)
end
+ def list_commits_by_oid(oids)
+ request = Gitaly::ListCommitsByOidRequest.new(repository: @gitaly_repo, oid: oids)
+
+ response = GitalyClient.call(@repository.storage, :commit_service, :list_commits_by_oid, request, timeout: GitalyClient.medium_timeout)
+ consume_commits_response(response)
+ rescue GRPC::Unknown # If no repository is found, happens mainly during testing
+ []
+ end
+
def commits_by_message(query, revision: '', path: '', limit: 1000, offset: 0)
request = Gitaly::CommitsByMessageRequest.new(
repository: @gitaly_repo,
diff --git a/lib/gitlab/github_import/importer/repository_importer.rb b/lib/gitlab/github_import/importer/repository_importer.rb
index 9cf2e7fd871..7dd68a0d1cd 100644
--- a/lib/gitlab/github_import/importer/repository_importer.rb
+++ b/lib/gitlab/github_import/importer/repository_importer.rb
@@ -29,7 +29,7 @@ module Gitlab
# this code, e.g. because we had to retry this job after
# `import_wiki?` raised a rate limit error. In this case we'll skip
# re-importing the main repository.
- if project.repository.empty_repo?
+ if project.empty_repo?
import_repository
else
true
diff --git a/package.json b/package.json
index 9e816e007ee..aa4e4e79f49 100644
--- a/package.json
+++ b/package.json
@@ -32,7 +32,14 @@
"core-js": "^2.4.1",
"cropper": "^2.3.0",
"css-loader": "^0.28.0",
- "d3": "^3.5.11",
+ "d3-array": "^1.2.1",
+ "d3-axis": "^1.0.8",
+ "d3-brush": "^1.0.4",
+ "d3-scale": "^1.0.7",
+ "d3-selection": "^1.2.0",
+ "d3-shape": "^1.2.0",
+ "d3-time": "^1.0.8",
+ "d3-time-format": "^2.1.1",
"deckar01-task_list": "^2.0.0",
"diff": "^3.4.0",
"document-register-element": "1.3.0",
@@ -72,6 +79,7 @@
"vue": "^2.5.8",
"vue-loader": "^13.5.0",
"vue-resource": "^1.3.4",
+ "vue-router": "^3.0.1",
"vue-template-compiler": "^2.5.8",
"vuex": "^3.0.1",
"webpack": "^3.5.5",
diff --git a/qa/README.md b/qa/README.md
index 1cfbbdd9d42..7f2dd39ff63 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -33,7 +33,14 @@ You can also supply specific tests to run as another parameter. For example, to
test the EE license specs, you can run:
```
-EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/ee
+EE_LICENSE="<YOUR LICENSE KEY>" bin/qa Test::Instance http://localhost qa/specs/features/ee
+```
+
+Since the arguments would be passed to `rspec`, you could use all `rspec`
+options there. For example, passing `--backtrace` and also line number:
+
+```
+bin/qa Test::Instance http://localhost qa/specs/features/login/standard_spec.rb:3 --backtrace
```
### Overriding the authenticated user
diff --git a/qa/qa.rb b/qa/qa.rb
index f473686d344..340f5e35c67 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -17,6 +17,8 @@ module QA
#
module Factory
autoload :Base, 'qa/factory/base'
+ autoload :Dependency, 'qa/factory/dependency'
+ autoload :Product, 'qa/factory/product'
module Resource
autoload :Sandbox, 'qa/factory/resource/sandbox'
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index 7b951a99b69..00851a7bece 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -1,15 +1,34 @@
module QA
module Factory
class Base
+ def fabricate!(*_args)
+ raise NotImplementedError
+ end
+
def self.fabricate!(*args)
- new.tap do |factory|
+ Factory::Product.populate!(new) do |factory|
yield factory if block_given?
- return factory.fabricate!(*args)
+
+ dependencies.each do |name, signature|
+ Factory::Dependency.new(name, factory, signature).build!
+ end
+
+ factory.fabricate!(*args)
end
end
- def fabricate!(*_args)
- raise NotImplementedError
+ def self.dependencies
+ @dependencies ||= {}
+ end
+
+ def self.dependency(factory, as:, &block)
+ as.tap do |name|
+ class_eval { attr_accessor name }
+
+ Dependency::Signature.new(factory, block).tap do |signature|
+ dependencies.store(name, signature)
+ end
+ end
end
end
end
diff --git a/qa/qa/factory/dependency.rb b/qa/qa/factory/dependency.rb
new file mode 100644
index 00000000000..d0e85a68237
--- /dev/null
+++ b/qa/qa/factory/dependency.rb
@@ -0,0 +1,38 @@
+module QA
+ module Factory
+ class Dependency
+ Signature = Struct.new(:factory, :block)
+
+ def initialize(name, factory, signature)
+ @name = name
+ @factory = factory
+ @signature = signature
+ end
+
+ def overridden?
+ !!@factory.public_send(@name)
+ end
+
+ def build!
+ return if overridden?
+
+ Builder.new(@signature).fabricate!.tap do |product|
+ @factory.public_send("#{@name}=", product)
+ end
+ end
+
+ class Builder
+ def initialize(signature)
+ @factory = signature.factory
+ @block = signature.block
+ end
+
+ def fabricate!
+ @factory.fabricate! do |factory|
+ @block&.call(factory)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
new file mode 100644
index 00000000000..df35bbbb443
--- /dev/null
+++ b/qa/qa/factory/product.rb
@@ -0,0 +1,26 @@
+require 'capybara/dsl'
+
+module QA
+ module Factory
+ class Product
+ include Capybara::DSL
+
+ def initialize(factory)
+ @factory = factory
+ @location = current_url
+ end
+
+ def visit!
+ visit @location
+ end
+
+ def self.populate!(factory)
+ raise ArgumentError unless block_given?
+
+ yield factory
+
+ new(factory)
+ end
+ end
+ end
+end
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 1d5375d8c76..2f4de4173d4 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -1,16 +1,13 @@
-require "pry-byebug"
-
module QA
module Factory
module Repository
class Push < Factory::Base
- PAGE_REGEX_CHECK =
- %r{\/#{Runtime::Namespace.sandbox_name}\/qa-test[^\/]+\/{1}[^\/]+\z}.freeze
+ attr_writer :file_name, :file_content, :commit_message, :branch_name
- attr_writer :file_name,
- :file_content,
- :commit_message,
- :branch_name
+ dependency Factory::Resource::Project, as: :project do |project|
+ project.name = 'project-with-code'
+ project.description = 'Project with repository'
+ end
def initialize
@file_name = 'file.txt'
@@ -20,12 +17,10 @@ module QA
end
def fabricate!
+ project.visit!
+
Git::Repository.perform do |repository|
repository.location = Page::Project::Show.act do
- unless PAGE_REGEX_CHECK.match(current_path)
- raise "To perform this scenario the current page should be project show."
- end
-
choose_repository_clone_http
repository_location
end
diff --git a/qa/qa/factory/resource/group.rb b/qa/qa/factory/resource/group.rb
index a081cd94d39..9f13e26f35c 100644
--- a/qa/qa/factory/resource/group.rb
+++ b/qa/qa/factory/resource/group.rb
@@ -4,17 +4,29 @@ module QA
class Group < Factory::Base
attr_writer :path, :description
+ dependency Factory::Resource::Sandbox, as: :sandbox
+
def initialize
@path = Runtime::Namespace.name
@description = "QA test run at #{Runtime::Namespace.time}"
end
def fabricate!
- Page::Group::New.perform do |group|
- group.set_path(@path)
- group.set_description(@description)
- group.set_visibility('Private')
- group.create
+ sandbox.visit!
+
+ Page::Group::Show.perform do |page|
+ if page.has_subgroup?(@path)
+ page.go_to_subgroup(@path)
+ else
+ page.go_to_new_subgroup
+
+ Page::Group::New.perform do |group|
+ group.set_path(@path)
+ group.set_description(@description)
+ group.set_visibility('Private')
+ group.create
+ end
+ end
end
end
end
diff --git a/qa/qa/factory/resource/project.rb b/qa/qa/factory/resource/project.rb
index 64fcfb084bb..07c2e3086d1 100644
--- a/qa/qa/factory/resource/project.rb
+++ b/qa/qa/factory/resource/project.rb
@@ -6,26 +6,17 @@ module QA
class Project < Factory::Base
attr_writer :description
+ dependency Factory::Resource::Group, as: :group
+
def name=(name)
@name = "#{name}-#{SecureRandom.hex(8)}"
+ @description = 'My awesome project'
end
def fabricate!
- Factory::Resource::Sandbox.fabricate!
-
- Page::Group::Show.perform do |page|
- if page.has_subgroup?(Runtime::Namespace.name)
- page.go_to_subgroup(Runtime::Namespace.name)
- else
- page.go_to_new_subgroup
+ group.visit!
- Factory::Resource::Group.fabricate! do |group|
- group.path = Runtime::Namespace.name
- end
- end
-
- page.go_to_new_project
- end
+ Page::Group::Show.act { go_to_new_project }
Page::Project::New.perform do |page|
page.choose_test_namespace
diff --git a/qa/qa/factory/resource/sandbox.rb b/qa/qa/factory/resource/sandbox.rb
index fd2177915c5..558da1c973b 100644
--- a/qa/qa/factory/resource/sandbox.rb
+++ b/qa/qa/factory/resource/sandbox.rb
@@ -6,18 +6,24 @@ module QA
# creating it if it doesn't yet exist.
#
class Sandbox < Factory::Base
+ def initialize
+ @name = Runtime::Namespace.sandbox_name
+ end
+
def fabricate!
Page::Main::Menu.act { go_to_groups }
Page::Dashboard::Groups.perform do |page|
- if page.has_group?(Runtime::Namespace.sandbox_name)
- page.go_to_group(Runtime::Namespace.sandbox_name)
+ if page.has_group?(@name)
+ page.go_to_group(@name)
else
page.go_to_new_group
- Resource::Group.fabricate! do |group|
- group.path = Runtime::Namespace.sandbox_name
- group.description = 'GitLab QA Sandbox'
+ Page::Group::New.perform do |group|
+ group.set_path(@name)
+ group.set_description('GitLab QA Sandbox')
+ group.set_visibility('Private')
+ group.create
end
end
end
diff --git a/qa/qa/page/group/show.rb b/qa/qa/page/group/show.rb
index 100e71ae157..0a16c07d64b 100644
--- a/qa/qa/page/group/show.rb
+++ b/qa/qa/page/group/show.rb
@@ -21,6 +21,7 @@ module QA
find('.dropdown-toggle').click
find("li[data-value='new-subgroup']").click
end
+
find("input[data-action='new-subgroup']").click
end
@@ -29,6 +30,7 @@ module QA
find('.dropdown-toggle').click
find("li[data-value='new-project']").click
end
+
find("input[data-action='new-project']").click
end
end
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index e47c769b015..4f6ffe14c9f 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -5,15 +5,10 @@ module QA
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
- Factory::Resource::Project.fabricate! do |scenario|
- scenario.name = 'project_with_code'
- scenario.description = 'project with repository'
- end
-
- Factory::Repository::Push.fabricate! do |scenario|
- scenario.file_name = 'README.md'
- scenario.file_content = '# This is test project'
- scenario.commit_message = 'Add README.md'
+ Factory::Repository::Push.fabricate! do |push|
+ push.file_name = 'README.md'
+ push.file_content = '# This is a test project'
+ push.commit_message = 'Add README.md'
end
Page::Project::Show.act do
@@ -22,7 +17,7 @@ module QA
end
expect(page).to have_content('README.md')
- expect(page).to have_content('This is test project')
+ expect(page).to have_content('This is a test project')
end
end
end
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
new file mode 100644
index 00000000000..a3ba0176819
--- /dev/null
+++ b/qa/spec/factory/base_spec.rb
@@ -0,0 +1,88 @@
+describe QA::Factory::Base do
+ describe '.fabricate!' do
+ subject { Class.new(described_class) }
+ let(:factory) { spy('factory') }
+ let(:product) { spy('product') }
+
+ before do
+ allow(QA::Factory::Product).to receive(:new).and_return(product)
+ end
+
+ it 'instantiates the factory and calls factory method' do
+ expect(subject).to receive(:new).and_return(factory)
+
+ subject.fabricate!('something')
+
+ expect(factory).to have_received(:fabricate!).with('something')
+ end
+
+ it 'returns fabrication product' do
+ allow(subject).to receive(:new).and_return(factory)
+ allow(factory).to receive(:fabricate!).and_return('something')
+
+ result = subject.fabricate!('something')
+
+ expect(result).to eq product
+ end
+
+ it 'yields factory before calling factory method' do
+ allow(subject).to receive(:new).and_return(factory)
+
+ subject.fabricate! do |factory|
+ factory.something!
+ end
+
+ expect(factory).to have_received(:something!).ordered
+ expect(factory).to have_received(:fabricate!).ordered
+ end
+ end
+
+ describe '.dependency' do
+ let(:dependency) { spy('dependency') }
+
+ before do
+ stub_const('Some::MyDependency', dependency)
+ end
+
+ subject do
+ Class.new(described_class) do
+ dependency Some::MyDependency, as: :mydep do |factory|
+ factory.something!
+ end
+ end
+ end
+
+ it 'appends a new dependency and accessors' do
+ expect(subject.dependencies).to be_one
+ end
+
+ it 'defines dependency accessors' do
+ expect(subject.new).to respond_to :mydep, :mydep=
+ end
+ end
+
+ describe 'building dependencies' do
+ let(:dependency) { double('dependency') }
+ let(:instance) { spy('instance') }
+
+ subject do
+ Class.new(described_class) do
+ dependency Some::MyDependency, as: :mydep
+ end
+ end
+
+ before do
+ stub_const('Some::MyDependency', dependency)
+
+ allow(subject).to receive(:new).and_return(instance)
+ allow(instance).to receive(:mydep).and_return(nil)
+ allow(QA::Factory::Product).to receive(:new)
+ end
+
+ it 'builds all dependencies first' do
+ expect(dependency).to receive(:fabricate!).once
+
+ subject.fabricate!
+ end
+ end
+end
diff --git a/qa/spec/factory/dependency_spec.rb b/qa/spec/factory/dependency_spec.rb
new file mode 100644
index 00000000000..32405415126
--- /dev/null
+++ b/qa/spec/factory/dependency_spec.rb
@@ -0,0 +1,59 @@
+describe QA::Factory::Dependency do
+ let(:dependency) { spy('dependency' ) }
+ let(:factory) { spy('factory') }
+ let(:block) { spy('block') }
+
+ let(:signature) do
+ double('signature', factory: dependency, block: block)
+ end
+
+ subject do
+ described_class.new(:mydep, factory, signature)
+ end
+
+ describe '#overridden?' do
+ it 'returns true if factory has overridden dependency' do
+ allow(factory).to receive(:mydep).and_return('something')
+
+ expect(subject).to be_overridden
+ end
+
+ it 'returns false if dependency has not been overridden' do
+ allow(factory).to receive(:mydep).and_return(nil)
+
+ expect(subject).not_to be_overridden
+ end
+ end
+
+ describe '#build!' do
+ context 'when dependency has been overridden' do
+ before do
+ allow(subject).to receive(:overridden?).and_return(true)
+ end
+
+ it 'does not fabricate dependency' do
+ subject.build!
+
+ expect(dependency).not_to have_received(:fabricate!)
+ end
+ end
+
+ context 'when dependency has not been overridden' do
+ before do
+ allow(subject).to receive(:overridden?).and_return(false)
+ end
+
+ it 'fabricates dependency' do
+ subject.build!
+
+ expect(dependency).to have_received(:fabricate!)
+ end
+
+ it 'sets product in the factory' do
+ subject.build!
+
+ expect(factory).to have_received(:mydep=).with(dependency)
+ end
+ end
+ end
+end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
new file mode 100644
index 00000000000..3d9e86a641b
--- /dev/null
+++ b/qa/spec/factory/product_spec.rb
@@ -0,0 +1,44 @@
+describe QA::Factory::Product do
+ let(:factory) { spy('factory') }
+ let(:product) { spy('product') }
+
+ describe '.populate!' do
+ it 'instantiates and yields factory' do
+ expect(described_class).to receive(:new).with(factory)
+
+ described_class.populate!(factory) do |instance|
+ instance.something = 'string'
+ end
+
+ expect(factory).to have_received(:something=).with('string')
+ end
+
+ it 'returns a fabrication product' do
+ expect(described_class).to receive(:new)
+ .with(factory).and_return(product)
+
+ result = described_class.populate!(factory) do |instance|
+ instance.something = 'string'
+ end
+
+ expect(result).to be product
+ end
+
+ it 'raises unless block given' do
+ expect { described_class.populate!(factory) }
+ .to raise_error ArgumentError
+ end
+ end
+
+ describe '.visit!' do
+ it 'makes it possible to visit fabrication product' do
+ allow_any_instance_of(described_class)
+ .to receive(:current_url).and_return('some url')
+ allow_any_instance_of(described_class)
+ .to receive(:visit).and_return('visited some url')
+
+ expect(described_class.new(factory).visit!)
+ .to eq 'visited some url'
+ end
+ end
+end
diff --git a/scripts/gitaly-test-spawn b/scripts/gitaly-test-spawn
index 8e05eca8d7e..ecb68c6acc6 100755
--- a/scripts/gitaly-test-spawn
+++ b/scripts/gitaly-test-spawn
@@ -1,7 +1,8 @@
#!/usr/bin/env ruby
gitaly_dir = 'tmp/tests/gitaly'
-env = { 'HOME' => File.expand_path('tmp/tests') }
+env = { 'HOME' => File.expand_path('tmp/tests'),
+ 'GEM_PATH' => Gem.path.join(':') }
args = %W[#{gitaly_dir}/gitaly #{gitaly_dir}/config.toml]
# Print the PID of the spawned process
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 1604a2da485..35ac999cc65 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -17,13 +17,10 @@ describe Projects::PipelinesController do
describe 'GET index.json' do
before do
- branch_head = project.commit
- parent = branch_head.parent
-
- create(:ci_empty_pipeline, status: 'pending', project: project, sha: branch_head.id)
- create(:ci_empty_pipeline, status: 'running', project: project, sha: branch_head.id)
- create(:ci_empty_pipeline, status: 'created', project: project, sha: parent.id)
- create(:ci_empty_pipeline, status: 'success', project: project, sha: parent.id)
+ %w(pending running created success).each_with_index do |status, index|
+ sha = project.commit("HEAD~#{index}")
+ create(:ci_empty_pipeline, status: status, project: project, sha: sha)
+ end
end
subject do
@@ -46,7 +43,7 @@ describe Projects::PipelinesController do
context 'when performing gitaly calls', :request_store do
it 'limits the Gitaly requests' do
- expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(8)
+ expect { subject }.to change { Gitlab::GitalyClient.get_request_count }.by(3)
end
end
end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index a9530becb65..70faf28e09d 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -12,7 +12,7 @@ feature 'Contributions Calendar', :js do
issue_params = { title: issue_title }
def get_cell_color_selector(contributions)
- activity_colors = %w[#ededed #acd5f2 #7fa8c9 #527ba0 #254e77]
+ activity_colors = ["#ededed", "rgb(172, 213, 242)", "rgb(127, 168, 201)", "rgb(82, 123, 160)", "rgb(37, 78, 119)"]
# We currently don't actually test the cases with contributions >= 20
activity_colors_index =
if contributions > 0 && contributions < 10
diff --git a/spec/features/help_pages_spec.rb b/spec/features/help_pages_spec.rb
index ab896a310be..0d04ed612c2 100644
--- a/spec/features/help_pages_spec.rb
+++ b/spec/features/help_pages_spec.rb
@@ -32,6 +32,24 @@ describe 'Help Pages' do
it_behaves_like 'help page', prefix: '/gitlab'
end
+
+ context 'quick link shortcuts', :js do
+ before do
+ visit help_path
+ end
+
+ it 'focuses search bar' do
+ find('.js-trigger-search-bar').click
+
+ expect(page).to have_selector('#search:focus')
+ end
+
+ it 'opens shortcuts help dialog' do
+ find('.js-trigger-shortcut').click
+
+ expect(page).to have_selector('#modal-shortcuts')
+ end
+ end
end
context 'in a production environment with version check enabled', :js do
diff --git a/spec/features/issues/issue_detail_spec.rb b/spec/features/issues/issue_detail_spec.rb
index 4224a8fe5d4..babb0285590 100644
--- a/spec/features/issues/issue_detail_spec.rb
+++ b/spec/features/issues/issue_detail_spec.rb
@@ -24,7 +24,7 @@ feature 'Issue Detail', :js do
visit project_issue_path(project, issue)
wait_for_requests
- click_link 'Edit'
+ page.find('.js-issuable-edit').click
fill_in 'issuable-title', with: 'issue title'
click_button 'Save'
wait_for_requests
diff --git a/spec/features/merge_requests/image_diff_notes.rb b/spec/features/merge_requests/image_diff_notes_spec.rb
index 021c4e03428..b53570835cb 100644
--- a/spec/features/merge_requests/image_diff_notes.rb
+++ b/spec/features/merge_requests/image_diff_notes_spec.rb
@@ -10,11 +10,10 @@ feature 'image diff notes', :js do
project.team << [user, :master]
sign_in user
- page.driver.set_cookie('sidebar_collapsed', 'true')
-
# Stub helper to return any blob file as image from public app folder.
# This is necessary to run this specs since we don't display repo images in capybara.
- allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_path).and_return('/apple-touch-icon.png')
+ allow_any_instance_of(DiffHelper).to receive(:diff_file_blob_raw_url).and_return('/apple-touch-icon.png')
+ allow_any_instance_of(DiffHelper).to receive(:diff_file_old_blob_raw_url).and_return('/favicon.ico')
end
context 'create commit diff notes' do
@@ -141,13 +140,13 @@ feature 'image diff notes', :js do
end
it 'allows expanding/collapsing the discussion notes' do
- page.all('.js-diff-notes-toggle')[0].trigger('click')
- page.all('.js-diff-notes-toggle')[1].trigger('click')
+ page.all('.js-diff-notes-toggle')[0].click
+ page.all('.js-diff-notes-toggle')[1].click
expect(page).not_to have_content('image diff test comment')
- page.all('.js-diff-notes-toggle')[0].trigger('click')
- page.all('.js-diff-notes-toggle')[1].trigger('click')
+ page.all('.js-diff-notes-toggle')[0].click
+ page.all('.js-diff-notes-toggle')[1].click
expect(page).to have_content('image diff test comment')
end
@@ -196,13 +195,31 @@ feature 'image diff notes', :js do
expect(find('.onion-skin-frame')['style']).to match('width: 228px; height: 240px;')
end
+
+ it 'resets onion skin view mode opacity when toggling between view modes' do
+ find('.view-modes-menu .onion-skin').click
+
+ # Simulate dragging onion-skin slider
+ drag_and_drop_by(find('.dragger'), -30, 0)
+
+ expect(find('.onion-skin-frame .frame.added', visible: false)['style']).not_to match('opacity: 1;')
+
+ find('.view-modes-menu .swipe').click
+ find('.view-modes-menu .onion-skin').click
+
+ expect(find('.onion-skin-frame .frame.added', visible: false)['style']).to match('opacity: 1;')
+ end
end
-end
-def create_image_diff_note
- find('.js-add-image-diff-note-button', match: :first).click
- page.all('.js-add-image-diff-note-button')[0].trigger('click')
- find('.diff-content .note-textarea').native.send_keys('image diff test comment')
- click_button 'Comment'
- wait_for_requests
+ def drag_and_drop_by(element, right_by, down_by)
+ page.driver.browser.action.drag_and_drop_by(element.native, right_by, down_by).perform
+ end
+
+ def create_image_diff_note
+ find('.js-add-image-diff-note-button', match: :first).click
+ page.all('.js-add-image-diff-note-button')[0].click
+ find('.diff-content .note-textarea').native.send_keys('image diff test comment')
+ click_button 'Comment'
+ wait_for_requests
+ end
end
diff --git a/spec/features/merge_requests/user_posts_notes_spec.rb b/spec/features/merge_requests/user_posts_notes_spec.rb
index f4c75a2f265..e17e9c2ccf5 100644
--- a/spec/features/merge_requests/user_posts_notes_spec.rb
+++ b/spec/features/merge_requests/user_posts_notes_spec.rb
@@ -66,6 +66,21 @@ describe 'Merge requests > User posts notes', :js do
end
end
+ describe 'when previewing a note' do
+ it 'shows the toolbar buttons when editing a note' do
+ page.within('.js-main-target-form') do
+ expect(page).to have_css('.md-header-toolbar.active')
+ end
+ end
+
+ it 'hides the toolbar buttons when previewing a note' do
+ find('.js-md-preview-button').click
+ page.within('.js-main-target-form') do
+ expect(page).not_to have_css('.md-header-toolbar.active')
+ end
+ end
+ end
+
describe 'when editing a note' do
it 'there should be a hidden edit form' do
is_expected.to have_css('.note-edit-form:not(.mr-note-edit-form)', visible: false, count: 1)
diff --git a/spec/features/milestone_spec.rb b/spec/features/milestone_spec.rb
index 27efc32c95b..9f24193a2ac 100644
--- a/spec/features/milestone_spec.rb
+++ b/spec/features/milestone_spec.rb
@@ -82,9 +82,9 @@ feature 'Milestone' do
milestone = create(:milestone, project: project, title: 8.7)
issue1 = create(:issue, project: project, milestone: milestone)
issue2 = create(:issue, project: project, milestone: milestone)
- issue1.spend_time(duration: 3600, user: user)
+ issue1.spend_time(duration: 3600, user_id: user.id)
issue1.save!
- issue2.spend_time(duration: 7200, user: user)
+ issue2.spend_time(duration: 7200, user_id: user.id)
issue2.save!
visit project_milestone_path(project, milestone)
diff --git a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
index 6c616bf0456..8ac9821b879 100644
--- a/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
+++ b/spec/features/projects/files/project_owner_sees_link_to_create_license_file_in_empty_project_spec.rb
@@ -2,15 +2,15 @@ require 'spec_helper'
feature 'project owner sees a link to create a license file in empty project', :js do
let(:project_master) { create(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:project_empty_repo) }
+
background do
- project.team << [project_master, :master]
+ project.add_master(project_master)
sign_in(project_master)
end
scenario 'project master creates a license file from a template' do
visit project_path(project)
- click_link 'Create empty bare repository'
click_on 'LICENSE'
expect(page).to have_content('New file')
@@ -26,8 +26,6 @@ feature 'project owner sees a link to create a license file in empty project', :
expect(file_content).to have_content("Copyright (c) #{Time.now.year} #{project.namespace.human_name}")
fill_in :commit_message, with: 'Add a LICENSE file', visible: true
- # Remove pre-receive hook so we can push without auth
- FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
click_button 'Commit changes'
expect(current_path).to eq(
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index 0257cd157c9..4319fc2746c 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -32,9 +32,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.js-issuable-actions') do
- click_on 'Edit'
- end
+ page.find('.js-issuable-edit').click
fill_in :'issuable-title', with: 'test issue title'
end
@@ -77,9 +75,7 @@ feature 'issuable templates', :js do
message: 'added issue template',
branch_name: 'master')
visit project_issue_path project, issue
- page.within('.js-issuable-actions') do
- click_on 'Edit'
- end
+ page.find('.js-issuable-edit').click
fill_in :'issuable-title', with: 'test issue title'
fill_in :'issue-description', with: prior_description
end
diff --git a/spec/features/projects/ref_switcher_spec.rb b/spec/features/projects/ref_switcher_spec.rb
deleted file mode 100644
index 33ccbc1a29f..00000000000
--- a/spec/features/projects/ref_switcher_spec.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-require 'rails_helper'
-
-feature 'Ref switcher', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public, :repository) }
-
- before do
- project.team << [user, :master]
- set_cookie('new_repo', 'true')
- sign_in(user)
- visit project_tree_path(project, 'master')
- end
-
- it 'allow user to change ref by enter key' do
- click_button 'master'
- wait_for_requests
-
- page.within '.project-refs-form' do
- input = find('input[type="search"]')
- input.set 'binary'
- wait_for_requests
-
- expect(find('.dropdown-content ul')).to have_selector('li', count: 7)
-
- page.within '.dropdown-content ul' do
- input.native.send_keys :enter
- end
- end
-
- expect(page).to have_title 'add-pdf-text-binary'
- end
-
- it "user selects ref with special characters" do
- click_button 'master'
- wait_for_requests
-
- page.within '.project-refs-form' do
- page.fill_in 'Search branches and tags', with: "'test'"
- click_link "'test'"
- end
-
- expect(page).to have_title "'test'"
- end
-
- context "create branch" do
- let(:input) { find('.js-new-branch-name') }
-
- before do
- click_button 'master'
- wait_for_requests
-
- page.within '.project-refs-form' do
- find(".dropdown-footer-list a").click
- end
- end
-
- it "shows error message for the invalid branch name" do
- input.set 'foo bar'
- click_button('Create')
- wait_for_requests
- expect(page).to have_content 'Branch name is invalid'
- end
-
- it "should create new branch properly" do
- input.set 'new-branch-name'
- click_button('Create')
- wait_for_requests
- expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name'
- end
-
- it "should create new branch by Enter key" do
- input.set 'new-branch-name-2'
- input.native.send_keys :enter
- wait_for_requests
- expect(find('.js-project-refs-dropdown')).to have_content 'new-branch-name-2'
- end
- end
-end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 8f06328962e..3f6d16c8acf 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -13,6 +13,14 @@ feature 'Multi-file editor new directory', :js do
visit project_tree_path(project, :master)
wait_for_requests
+
+ click_link('Multi Edit')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
end
it 'creates directory in current directory' do
@@ -21,17 +29,29 @@ feature 'Multi-file editor new directory', :js do
click_link('New directory')
page.within('.modal') do
- find('.form-control').set('foldername')
+ find('.form-control').set('folder name')
click_button('Create directory')
end
+ find('.add-to-tree').click
+
+ click_link('New file')
+
+ page.within('.modal-dialog') do
+ find('.form-control').set('file name')
+
+ click_button('Create file')
+ end
+
+ wait_for_requests
+
find('.multi-file-commit-panel-collapse-btn').click
- fill_in('commit-message', with: 'commit message')
+ fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
- expect(page).to have_selector('td', text: 'commit message')
+ expect(page).to have_content('folder name')
end
end
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index bdebc12ef47..ba71eef07f4 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -13,6 +13,14 @@ feature 'Multi-file editor new file', :js do
visit project_tree_path(project, :master)
wait_for_requests
+
+ click_link('Multi Edit')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
end
it 'creates file in current directory' do
@@ -21,17 +29,19 @@ feature 'Multi-file editor new file', :js do
click_link('New file')
page.within('.modal') do
- find('.form-control').set('filename')
+ find('.form-control').set('file name')
click_button('Create file')
end
+ wait_for_requests
+
find('.multi-file-commit-panel-collapse-btn').click
- fill_in('commit-message', with: 'commit message')
+ fill_in('commit-message', with: 'commit message ide')
click_button('Commit')
- expect(page).to have_selector('td', text: 'commit message')
+ expect(page).to have_content('file name')
end
end
diff --git a/spec/features/projects/tree/upload_file_spec.rb b/spec/features/projects/tree/upload_file_spec.rb
index d4e57d1ecfa..9fbb1dbd0e8 100644
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ b/spec/features/projects/tree/upload_file_spec.rb
@@ -15,6 +15,14 @@ feature 'Multi-file editor upload file', :js do
visit project_tree_path(project, :master)
wait_for_requests
+
+ click_link('Multi Edit')
+
+ wait_for_requests
+ end
+
+ after do
+ set_cookie('new_repo', 'false')
end
it 'uploads text file' do
@@ -41,6 +49,5 @@ feature 'Multi-file editor upload file', :js do
expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
expect(page).not_to have_selector('.monaco-editor')
- expect(page).to have_content('The source could not be displayed for this temporary file.')
end
end
diff --git a/spec/features/tags/master_views_tags_spec.rb b/spec/features/tags/master_views_tags_spec.rb
index 9edc7ced163..4662367d843 100644
--- a/spec/features/tags/master_views_tags_spec.rb
+++ b/spec/features/tags/master_views_tags_spec.rb
@@ -4,18 +4,17 @@ feature 'Master views tags' do
let(:user) { create(:user) }
before do
- project.team << [user, :master]
+ project.add_master(user)
sign_in(user)
end
context 'when project has no tags' do
let(:project) { create(:project_empty_repo) }
+
before do
visit project_path(project)
click_on 'README'
fill_in :commit_message, with: 'Add a README file', visible: true
- # Remove pre-receive hook so we can push without auth
- FileUtils.rm_f(File.join(project.repository.path, 'hooks', 'pre-receive'))
click_button 'Commit changes'
visit project_tags_path(project)
end
diff --git a/spec/finders/group_descendants_finder_spec.rb b/spec/finders/group_descendants_finder_spec.rb
index 074914420a1..ae050f36b4a 100644
--- a/spec/finders/group_descendants_finder_spec.rb
+++ b/spec/finders/group_descendants_finder_spec.rb
@@ -73,6 +73,41 @@ describe GroupDescendantsFinder do
expect(finder.execute).to contain_exactly(matching_project)
end
end
+
+ context 'sorting by name' do
+ let!(:project1) { create(:project, namespace: group, name: 'a', path: 'project-a') }
+ let!(:project2) { create(:project, namespace: group, name: 'z', path: 'project-z') }
+ let(:params) do
+ {
+ sort: 'name_asc'
+ }
+ end
+
+ it 'sorts elements by name' do
+ expect(subject.execute).to eq(
+ [
+ project1,
+ project2
+ ]
+ )
+ end
+
+ context 'with nested groups', :nested_groups do
+ let!(:subgroup1) { create(:group, parent: group, name: 'a', path: 'sub-a') }
+ let!(:subgroup2) { create(:group, parent: group, name: 'z', path: 'sub-z') }
+
+ it 'sorts elements by name' do
+ expect(subject.execute).to eq(
+ [
+ subgroup1,
+ subgroup2,
+ project1,
+ project2
+ ]
+ )
+ end
+ end
+ end
end
context 'with nested groups', :nested_groups do
diff --git a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
index 861f26e162f..6599839a526 100644
--- a/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
+++ b/spec/javascripts/graphs/stat_graph_contributors_graph_spec.js
@@ -1,8 +1,10 @@
/* eslint-disable quotes, jasmine/no-suite-dupes, vars-on-top, no-var */
-
-import d3 from 'd3';
+import { scaleLinear, scaleTime } from 'd3-scale';
+import { timeParse } from 'd3-time-format';
import { ContributorsGraph, ContributorsMasterGraph } from '~/graphs/stat_graph_contributors_graph';
+const d3 = { scaleLinear, scaleTime, timeParse };
+
describe("ContributorsGraph", function () {
describe("#set_x_domain", function () {
it("set the x_domain", function () {
@@ -53,7 +55,7 @@ describe("ContributorsGraph", function () {
it("sets the instance's x domain using the prototype's x_domain", function () {
ContributorsGraph.prototype.x_domain = 20;
var instance = new ContributorsGraph();
- instance.x = d3.time.scale().range([0, 100]).clamp(true);
+ instance.x = d3.scaleTime().range([0, 100]).clamp(true);
spyOn(instance.x, 'domain');
instance.set_x_domain();
expect(instance.x.domain).toHaveBeenCalledWith(20);
@@ -64,7 +66,7 @@ describe("ContributorsGraph", function () {
it("sets the instance's y domain using the prototype's y_domain", function () {
ContributorsGraph.prototype.y_domain = 30;
var instance = new ContributorsGraph();
- instance.y = d3.scale.linear().range([100, 0]).nice();
+ instance.y = d3.scaleLinear().range([100, 0]).nice();
spyOn(instance.y, 'domain');
instance.set_y_domain();
expect(instance.y.domain).toHaveBeenCalledWith(30);
@@ -118,7 +120,7 @@ describe("ContributorsMasterGraph", function () {
describe("#parse_dates", function () {
it("parses the dates", function () {
var graph = new ContributorsMasterGraph();
- var parseDate = d3.time.format("%Y-%m-%d").parse;
+ var parseDate = d3.timeParse("%Y-%m-%d");
var data = [{ date: "2013-01-01" }, { date: "2012-12-15" }];
var correct = [{ date: parseDate(data[0].date) }, { date: parseDate(data[1].date) }];
graph.parse_dates(data);
diff --git a/spec/javascripts/merge_request_tabs_spec.js b/spec/javascripts/merge_request_tabs_spec.js
index 050f0ea9ebd..a6be474805b 100644
--- a/spec/javascripts/merge_request_tabs_spec.js
+++ b/spec/javascripts/merge_request_tabs_spec.js
@@ -290,15 +290,18 @@ import 'vendor/jquery.scrollTo';
$('body').removeAttr('data-page');
});
- it('requires an absolute pathname', function () {
- spyOn($, 'ajax').and.callFake(function (options) {
- expect(options.url).toEqual('/foo/bar/merge_requests/1/diffs.json');
+ it('triggers Ajax request to JSON endpoint', function (done) {
+ const url = '/foo/bar/merge_requests/1/diffs';
+ spyOn(this.class, 'ajaxGet').and.callFake((options) => {
+ expect(options.url).toEqual(`${url}.json`);
+ done();
});
- this.class.loadDiff('/foo/bar/merge_requests/1/diffs');
+ this.class.loadDiff(url);
});
- it('triggers scroll event when diff already loaded', function () {
+ it('triggers scroll event when diff already loaded', function (done) {
+ spyOn(this.class, 'ajaxGet').and.callFake(() => done.fail());
spyOn(document, 'dispatchEvent');
this.class.diffsLoaded = true;
@@ -307,6 +310,7 @@ import 'vendor/jquery.scrollTo';
expect(
document.dispatchEvent,
).toHaveBeenCalledWith(new CustomEvent('scroll'));
+ done();
});
describe('with inline diff', () => {
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
index f750061a6a1..c4d3866c922 100644
--- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
+++ b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import listCollapsed from '~/repo/components/commit_sidebar/list_collapsed.vue';
+import store from '~/ide/stores';
+import listCollapsed from '~/ide/components/commit_sidebar/list_collapsed.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers';
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
index 18c9b46fcd9..fc7c9ae9dd7 100644
--- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
+++ b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import listItem from '~/repo/components/commit_sidebar/list_item.vue';
+import listItem from '~/ide/components/commit_sidebar/list_item.vue';
import mountComponent from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers';
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
index df7e3c5de21..cb5240ad118 100644
--- a/spec/javascripts/repo/components/commit_sidebar/list_spec.js
+++ b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import commitSidebarList from '~/repo/components/commit_sidebar/list.vue';
+import store from '~/ide/stores';
+import commitSidebarList from '~/ide/components/commit_sidebar/list.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file } from '../../helpers';
@@ -13,8 +13,11 @@ describe('Multi-file editor commit sidebar list', () => {
vm = createComponentWithStore(Component, store, {
title: 'Staged',
fileList: [],
- collapsed: false,
- }).$mount();
+ });
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
});
afterEach(() => {
@@ -43,30 +46,14 @@ describe('Multi-file editor commit sidebar list', () => {
describe('collapsed', () => {
beforeEach((done) => {
- vm.collapsed = true;
+ vm.$store.state.rightPanelCollapsed = true;
Vue.nextTick(done);
});
- it('adds collapsed class', () => {
- expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
- });
-
it('hides list', () => {
expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
expect(vm.$el.querySelector('.help-block')).toBeNull();
});
-
- it('hides collapse button', () => {
- expect(vm.$el.querySelector('.multi-file-commit-panel-collapse-btn')).toBeNull();
- });
- });
-
- it('clicking toggle collapse button emits toggle event', () => {
- spyOn(vm, '$emit');
-
- vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
-
- expect(vm.$emit).toHaveBeenCalledWith('toggleCollapsed');
});
});
diff --git a/spec/javascripts/repo/components/ide_context_bar_spec.js b/spec/javascripts/repo/components/ide_context_bar_spec.js
new file mode 100644
index 00000000000..3f8f37d2343
--- /dev/null
+++ b/spec/javascripts/repo/components/ide_context_bar_spec.js
@@ -0,0 +1,49 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideContextBar from '~/ide/components/ide_context_bar.vue';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+
+describe('Multi-file editor right context bar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideContextBar);
+
+ vm = createComponentWithStore(Component, store);
+
+ vm.$store.state.rightPanelCollapsed = false;
+
+ vm.$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('collapsed', () => {
+ beforeEach((done) => {
+ vm.$store.state.rightPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.querySelector('.is-collapsed')).not.toBeNull();
+ });
+
+ it('shows correct icon', () => {
+ expect(vm.currentIcon).toBe('angle-double-left');
+ });
+ });
+
+ it('clicking toggle collapse button collapses the bar', () => {
+ spyOn(vm, 'setPanelCollapsedStatus').and.returnValue(Promise.resolve());
+
+ vm.$el.querySelector('.multi-file-commit-panel-collapse-btn').click();
+
+ expect(vm.setPanelCollapsedStatus).toHaveBeenCalledWith({
+ side: 'right',
+ collapsed: true,
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_sidebar_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js
index df7cf8aabbb..b6f70f585cd 100644
--- a/spec/javascripts/repo/components/repo_sidebar_spec.js
+++ b/spec/javascripts/repo/components/ide_repo_tree_spec.js
@@ -1,20 +1,26 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoSidebar from '~/repo/components/repo_sidebar.vue';
+import store from '~/ide/stores';
+import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
import { file, resetStore } from '../helpers';
-describe('RepoSidebar', () => {
+describe('IdeRepoTree', () => {
let vm;
beforeEach(() => {
- const RepoSidebar = Vue.extend(repoSidebar);
+ const IdeRepoTree = Vue.extend(ideRepoTree);
- vm = new RepoSidebar({
+ vm = new IdeRepoTree({
store,
+ propsData: {
+ treeId: 'abcproject/mybranch',
+ },
});
+ vm.$store.state.currentBranch = 'master';
vm.$store.state.isRoot = true;
- vm.$store.state.tree.push(file());
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
vm.$mount();
});
@@ -26,13 +32,9 @@ describe('RepoSidebar', () => {
});
it('renders a sidebar', () => {
- const thead = vm.$el.querySelector('thead');
const tbody = vm.$el.querySelector('tbody');
expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
- expect(thead.querySelector('.name').textContent.trim()).toEqual('Name');
- expect(thead.querySelector('.last-commit').textContent.trim()).toEqual('Last commit');
- expect(thead.querySelector('.last-update').textContent.trim()).toEqual('Last update');
expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
expect(tbody.querySelector('.prev-directory')).toBeFalsy();
expect(tbody.querySelector('.loading-file')).toBeFalsy();
@@ -40,7 +42,6 @@ describe('RepoSidebar', () => {
});
it('renders 5 loading files if tree is loading', (done) => {
- vm.$store.state.tree = [];
vm.$store.state.loading = true;
Vue.nextTick(() => {
diff --git a/spec/javascripts/repo/components/ide_side_bar_spec.js b/spec/javascripts/repo/components/ide_side_bar_spec.js
new file mode 100644
index 00000000000..30e45169205
--- /dev/null
+++ b/spec/javascripts/repo/components/ide_side_bar_spec.js
@@ -0,0 +1,43 @@
+import Vue from 'vue';
+import store from '~/ide/stores';
+import ideSidebar from '~/ide/components/ide_side_bar.vue';
+import { resetStore } from '../helpers';
+import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
+
+describe('IdeSidebar', () => {
+ let vm;
+
+ beforeEach(() => {
+ const Component = Vue.extend(ideSidebar);
+
+ vm = createComponentWithStore(Component, store).$mount();
+
+ vm.$store.state.leftPanelCollapsed = false;
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+
+ resetStore(vm.$store);
+ });
+
+ it('renders a sidebar', () => {
+ expect(vm.$el.querySelector('.multi-file-commit-panel-inner')).not.toBeNull();
+ });
+
+ describe('collapsed', () => {
+ beforeEach((done) => {
+ vm.$store.state.leftPanelCollapsed = true;
+
+ Vue.nextTick(done);
+ });
+
+ it('adds collapsed class', () => {
+ expect(vm.$el.classList).toContain('is-collapsed');
+ });
+
+ it('shows correct icon', () => {
+ expect(vm.currentIcon).toBe('angle-double-right');
+ });
+ });
+});
diff --git a/spec/javascripts/repo/components/repo_spec.js b/spec/javascripts/repo/components/ide_spec.js
index b32d2c13af8..20b8dc25dcb 100644
--- a/spec/javascripts/repo/components/repo_spec.js
+++ b/spec/javascripts/repo/components/ide_spec.js
@@ -1,14 +1,14 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repo from '~/repo/components/repo.vue';
+import store from '~/ide/stores';
+import ide from '~/ide/components/ide.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../helpers';
-describe('repo component', () => {
+describe('ide component', () => {
let vm;
beforeEach(() => {
- const Component = Vue.extend(repo);
+ const Component = Vue.extend(ide);
vm = createComponentWithStore(Component, store).$mount();
});
@@ -24,7 +24,9 @@ describe('repo component', () => {
});
it('renders panel right when files are open', (done) => {
- vm.$store.state.tree.push(file());
+ vm.$store.state.trees['abcproject/mybranch'] = {
+ tree: [file()],
+ };
Vue.nextTick(() => {
expect(vm.$el.querySelector('.panel-right')).toBeNull();
diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js
index 9a705a1f0ed..cd1d073ec18 100644
--- a/spec/javascripts/repo/components/new_branch_form_spec.js
+++ b/spec/javascripts/repo/components/new_branch_form_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import newBranchForm from '~/repo/components/new_branch_form.vue';
+import store from '~/ide/stores';
+import newBranchForm from '~/ide/components/new_branch_form.vue';
import { createComponentWithStore } from '../../helpers/vue_mount_component_helper';
import { resetStore } from '../helpers';
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
index 93b10fc1fee..b001c1655b4 100644
--- a/spec/javascripts/repo/components/new_dropdown/index_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/index_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import newDropdown from '~/repo/components/new_dropdown/index.vue';
+import store from '~/ide/stores';
+import newDropdown from '~/ide/components/new_dropdown/index.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
@@ -10,8 +10,12 @@ describe('new dropdown component', () => {
beforeEach(() => {
const component = Vue.extend(newDropdown);
- vm = createComponentWithStore(component, store);
+ vm = createComponentWithStore(component, store, {
+ branch: 'master',
+ path: '',
+ });
+ vm.$store.state.currentProjectId = 'abcproject';
vm.$store.state.path = '';
vm.$mount();
@@ -23,9 +27,10 @@ describe('new dropdown component', () => {
resetStore(vm.$store);
});
- it('renders new file and new directory links', () => {
+ it('renders new file, upload and new directory links', () => {
expect(vm.$el.querySelectorAll('a')[0].textContent.trim()).toBe('New file');
- expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('New directory');
+ expect(vm.$el.querySelectorAll('a')[1].textContent.trim()).toBe('Upload file');
+ expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
});
describe('createNewItem', () => {
@@ -36,7 +41,7 @@ describe('new dropdown component', () => {
});
it('sets modalType to tree when new directory is clicked', () => {
- vm.$el.querySelectorAll('a')[1].click();
+ vm.$el.querySelectorAll('a')[2].click();
expect(vm.modalType).toBe('tree');
});
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
index 1ff7590ec79..233cca06ed0 100644
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
@@ -1,12 +1,42 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import modal from '~/repo/components/new_dropdown/modal.vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import modal from '~/ide/components/new_dropdown/modal.vue';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { file, resetStore } from '../../helpers';
describe('new file modal component', () => {
const Component = Vue.extend(modal);
let vm;
+ let projectTree;
+
+ beforeEach(() => {
+ spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
+ data: {
+ id: '123',
+ },
+ }));
+
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: {
+ id: '123branch',
+ },
+ }));
+
+ spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () => Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }));
+ });
afterEach(() => {
vm.$destroy();
@@ -17,12 +47,26 @@ describe('new file modal component', () => {
['tree', 'blob'].forEach((type) => {
describe(type, () => {
beforeEach(() => {
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ store.state.trees = [];
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ projectTree = store.state.trees['abcproject/mybranch'];
+ store.state.currentProjectId = 'abcproject';
+
vm = createComponentWithStore(Component, store, {
type,
+ branchId: 'master',
path: '',
- }).$mount();
+ parent: projectTree,
+ });
vm.entryName = 'testing';
+
+ vm.$mount();
});
it(`sets modal title as ${type}`, () => {
@@ -50,6 +94,9 @@ describe('new file modal component', () => {
vm.createEntryInStore();
expect(vm.createTempEntry).toHaveBeenCalledWith({
+ projectId: 'abcproject',
+ branchId: 'master',
+ parent: projectTree,
name: 'testing',
type,
});
@@ -76,31 +123,18 @@ describe('new file modal component', () => {
});
it('opens newly created file', (done) => {
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(vm.$store.state.openFiles.length).toBe(1);
- expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
-
- done();
- });
- });
-
- it(`creates ${type} in the current stores path`, (done) => {
- vm.$store.state.path = 'app';
-
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(vm.$store.state.tree[0].path).toBe('app/testing');
- expect(vm.$store.state.tree[0].name).toBe('testing');
+ if (type === 'blob') {
+ vm.createEntryInStore();
- if (type === 'tree') {
- expect(vm.$store.state.tree[0].tree.length).toBe(1);
- }
+ setTimeout(() => {
+ expect(vm.$store.state.openFiles.length).toBe(1);
+ expect(vm.$store.state.openFiles[0].name).toBe(type === 'blob' ? 'testing' : '.gitkeep');
+ done();
+ });
+ } else {
done();
- });
+ }
});
if (type === 'blob') {
@@ -108,25 +142,27 @@ describe('new file modal component', () => {
vm.createEntryInStore();
setTimeout(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('testing');
- expect(vm.$store.state.tree[0].type).toBe('blob');
- expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('testing');
+ expect(baseTree[0].type).toBe('blob');
+ expect(baseTree[0].tempFile).toBeTruthy();
done();
});
});
it('does not create temp file when file already exists', (done) => {
- vm.$store.state.tree.push(file('testing', '1', type));
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('testing', '1', type));
vm.createEntryInStore();
setTimeout(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('testing');
- expect(vm.$store.state.tree[0].type).toBe('blob');
- expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('testing');
+ expect(baseTree[0].type).toBe('blob');
+ expect(baseTree[0].tempFile).toBeFalsy();
done();
});
@@ -135,48 +171,47 @@ describe('new file modal component', () => {
it('creates new tree', () => {
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('testing');
- expect(vm.$store.state.tree[0].type).toBe('tree');
- expect(vm.$store.state.tree[0].tempFile).toBeTruthy();
- expect(vm.$store.state.tree[0].tree.length).toBe(1);
- expect(vm.$store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('testing');
+ expect(baseTree[0].type).toBe('tree');
+ expect(baseTree[0].tempFile).toBeTruthy();
});
it('creates multiple trees when entryName has slashes', () => {
vm.entryName = 'app/test';
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('app');
- expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
- expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('app');
});
it('creates tree in existing tree', () => {
- vm.$store.state.tree.push(file('app', '1', 'tree'));
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app/test';
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('app');
- expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
- expect(vm.$store.state.tree[0].tree[0].tempFile).toBeTruthy();
- expect(vm.$store.state.tree[0].tree[0].name).toBe('test');
- expect(vm.$store.state.tree[0].tree[0].tree[0].name).toBe('.gitkeep');
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('app');
+ expect(baseTree[0].tempFile).toBeFalsy();
+ expect(baseTree[0].tree[0].tempFile).toBeTruthy();
+ expect(baseTree[0].tree[0].name).toBe('test');
});
it('does not create new tree when already exists', () => {
- vm.$store.state.tree.push(file('app', '1', 'tree'));
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('app', '1', 'tree'));
vm.entryName = 'app';
vm.createEntryInStore();
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe('app');
- expect(vm.$store.state.tree[0].tempFile).toBeFalsy();
- expect(vm.$store.state.tree[0].tree.length).toBe(0);
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe('app');
+ expect(baseTree[0].tempFile).toBeFalsy();
+ expect(baseTree[0].tree.length).toBe(0);
});
}
});
@@ -188,6 +223,8 @@ describe('new file modal component', () => {
vm = createComponentWithStore(Component, store, {
type: 'tree',
+ projectId: 'abcproject',
+ branchId: 'master',
path: '',
}).$mount('.js-test');
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
index bf7893029b1..788c08e5279 100644
--- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js
+++ b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
@@ -1,19 +1,61 @@
import Vue from 'vue';
-import upload from '~/repo/components/new_dropdown/upload.vue';
-import store from '~/repo/stores';
+import upload from '~/ide/components/new_dropdown/upload.vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { createComponentWithStore } from '../../../helpers/vue_mount_component_helper';
import { resetStore } from '../../helpers';
describe('new dropdown upload', () => {
let vm;
+ let projectTree;
beforeEach(() => {
+ spyOn(service, 'getProjectData').and.returnValue(Promise.resolve({
+ data: {
+ id: '123',
+ },
+ }));
+
+ spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
+ commit: {
+ id: '123branch',
+ },
+ }));
+
+ spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () => Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }));
+
const Component = Vue.extend(upload);
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ store.state.currentProjectId = 'abcproject';
+ store.state.trees = [];
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ projectTree = store.state.trees['abcproject/mybranch'];
+
vm = createComponentWithStore(Component, store, {
+ branchId: 'master',
path: '',
+ parent: projectTree,
});
+ vm.entryName = 'testing';
+
vm.$mount();
});
@@ -65,23 +107,33 @@ describe('new dropdown upload', () => {
vm.createFile(target, file, true);
vm.$nextTick(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe(file.name);
- expect(vm.$store.state.tree[0].content).toBe(target.result);
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe(file.name);
+ expect(baseTree[0].content).toBe(target.result);
done();
});
});
it('creates new file in path', (done) => {
- vm.$store.state.path = 'testing';
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ const tree = {
+ type: 'tree',
+ name: 'testing',
+ path: 'testing',
+ tree: [],
+ };
+ baseTree.push(tree);
+
+ vm.parent = tree;
vm.createFile(target, file, true);
vm.$nextTick(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe(file.name);
- expect(vm.$store.state.tree[0].content).toBe(target.result);
- expect(vm.$store.state.tree[0].path).toBe(`testing/${file.name}`);
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].tree[0].name).toBe(file.name);
+ expect(baseTree[0].tree[0].content).toBe(target.result);
+ expect(baseTree[0].tree[0].path).toBe(`testing/${file.name}`);
done();
});
@@ -91,10 +143,11 @@ describe('new dropdown upload', () => {
vm.createFile(binaryTarget, file, false);
vm.$nextTick(() => {
- expect(vm.$store.state.tree.length).toBe(1);
- expect(vm.$store.state.tree[0].name).toBe(file.name);
- expect(vm.$store.state.tree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
- expect(vm.$store.state.tree[0].base64).toBe(true);
+ const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].name).toBe(file.name);
+ expect(baseTree[0].content).toBe(binaryTarget.result.split('base64,')[1]);
+ expect(baseTree[0].base64).toBe(true);
done();
});
diff --git a/spec/javascripts/repo/components/repo_commit_section_spec.js b/spec/javascripts/repo/components/repo_commit_section_spec.js
index 72712e058e5..cd93fb3ccbf 100644
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ b/spec/javascripts/repo/components/repo_commit_section_spec.js
@@ -1,8 +1,8 @@
import Vue from 'vue';
import * as urlUtils from '~/lib/utils/url_utility';
-import store from '~/repo/stores';
-import service from '~/repo/services';
-import repoCommitSection from '~/repo/components/repo_commit_section.vue';
+import store from '~/ide/stores';
+import service from '~/ide/services';
+import repoCommitSection from '~/ide/components/repo_commit_section.vue';
import getSetTimeoutPromise from '../../helpers/set_timeout_promise_helper';
import { file, resetStore } from '../helpers';
@@ -16,6 +16,18 @@ describe('RepoCommitSection', () => {
store,
}).$mount();
+ comp.$store.state.currentProjectId = 'abcproject';
+ comp.$store.state.currentBranchId = 'master';
+ comp.$store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ comp.$store.state.rightPanelCollapsed = false;
comp.$store.state.currentBranch = 'master';
comp.$store.state.openFiles = [file(), file()];
comp.$store.state.openFiles.forEach(f => Object.assign(f, {
@@ -29,7 +41,19 @@ describe('RepoCommitSection', () => {
beforeEach((done) => {
vm = createComponent();
- vm.collapsed = false;
+ spyOn(service, 'getTreeData').and.returnValue(Promise.resolve({
+ headers: {
+ 'page-title': 'test',
+ },
+ json: () => Promise.resolve({
+ last_commit_path: 'last_commit_path',
+ parent_tree_url: 'parent_tree_url',
+ path: '/',
+ trees: [{ name: 'tree' }],
+ blobs: [{ name: 'blob' }],
+ submodules: [{ name: 'submodule' }],
+ }),
+ }));
Vue.nextTick(done);
});
@@ -45,7 +69,6 @@ describe('RepoCommitSection', () => {
const submitCommit = vm.$el.querySelector('form .btn');
expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
- expect(vm.$el.querySelector('.multi-file-commit-panel-section header').textContent.trim()).toEqual('Staged');
expect(changedFileElements.length).toEqual(2);
changedFileElements.forEach((changedFile, i) => {
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
index 44018464b35..2895b794506 100644
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ b/spec/javascripts/repo/components/repo_edit_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoEditButton from '~/repo/components/repo_edit_button.vue';
+import store from '~/ide/stores';
+import repoEditButton from '~/ide/components/repo_edit_button.vue';
import { file, resetStore } from '../helpers';
describe('RepoEditButton', () => {
@@ -32,7 +32,7 @@ describe('RepoEditButton', () => {
vm.$mount();
expect(vm.$el.querySelector('.btn')).not.toBeNull();
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
});
it('renders edit button with cancel text', () => {
@@ -50,7 +50,7 @@ describe('RepoEditButton', () => {
vm.$el.querySelector('.btn').click();
vm.$nextTick(() => {
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
+ expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
done();
});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
index 81158cad639..e7b2ed08acd 100644
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ b/spec/javascripts/repo/components/repo_editor_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoEditor from '~/repo/components/repo_editor.vue';
-import monacoLoader from '~/repo/monaco_loader';
+import store from '~/ide/stores';
+import repoEditor from '~/ide/components/repo_editor.vue';
+import monacoLoader from '~/ide/monaco_loader';
import { file, resetStore } from '../helpers';
describe('RepoEditor', () => {
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
index d6e255e4810..115569a9117 100644
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ b/spec/javascripts/repo/components/repo_file_buttons_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoFileButtons from '~/repo/components/repo_file_buttons.vue';
+import store from '~/ide/stores';
+import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
import { file, resetStore } from '../helpers';
describe('RepoFileButtons', () => {
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
index bf9181fb09c..e8b370f97b4 100644
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ b/spec/javascripts/repo/components/repo_file_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoFile from '~/repo/components/repo_file.vue';
+import store from '~/ide/stores';
+import repoFile from '~/ide/components/repo_file.vue';
import { file, resetStore } from '../helpers';
describe('RepoFile', () => {
@@ -35,11 +35,10 @@ describe('RepoFile', () => {
const fileIcon = vm.$el.querySelector('.file-icon');
expect(vm.$el.querySelector(`.${vm.file.icon}`).style.marginLeft).toEqual('0px');
- expect(name.href).toMatch(`/${vm.file.url}`);
+ expect(name.href).toMatch('');
expect(name.textContent.trim()).toEqual(vm.file.name);
expect(fileIcon.classList.contains(vm.file.icon)).toBeTruthy();
expect(fileIcon.style.marginLeft).toEqual(`${vm.file.level * 10}px`);
- expect(vm.$el.querySelectorAll('.animation-container').length).toBe(2);
});
it('does render if hasFiles is true and is loading tree', () => {
@@ -75,16 +74,16 @@ describe('RepoFile', () => {
});
});
- it('fires clickedTreeRow when the link is clicked', () => {
+ it('fires clickFile when the link is clicked', () => {
vm = createComponent({
file: file(),
});
- spyOn(vm, 'clickedTreeRow');
+ spyOn(vm, 'clickFile');
vm.$el.click();
- expect(vm.clickedTreeRow).toHaveBeenCalledWith(vm.file);
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.file);
});
describe('submodule', () => {
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
index 031f2a9c0b2..18366fb89bc 100644
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ b/spec/javascripts/repo/components/repo_loading_file_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoLoadingFile from '~/repo/components/repo_loading_file.vue';
+import store from '~/ide/stores';
+import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
import { resetStore } from '../helpers';
describe('RepoLoadingFile', () => {
@@ -48,6 +48,7 @@ describe('RepoLoadingFile', () => {
it('renders 1 column of animated LoC if isMini', (done) => {
vm = createComponent();
+ vm.$store.state.leftPanelCollapsed = true;
vm.$store.state.openFiles.push('test');
vm.$nextTick(() => {
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
index 7f82ae36a64..ff26cab2262 100644
--- a/spec/javascripts/repo/components/repo_prev_directory_spec.js
+++ b/spec/javascripts/repo/components/repo_prev_directory_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoPrevDirectory from '~/repo/components/repo_prev_directory.vue';
+import store from '~/ide/stores';
+import repoPrevDirectory from '~/ide/components/repo_prev_directory.vue';
import { resetStore } from '../helpers';
describe('RepoPrevDirectory', () => {
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
index 8d1a87494cf..e90837e4cb2 100644
--- a/spec/javascripts/repo/components/repo_preview_spec.js
+++ b/spec/javascripts/repo/components/repo_preview_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoPreview from '~/repo/components/repo_preview.vue';
+import store from '~/ide/stores';
+import repoPreview from '~/ide/components/repo_preview.vue';
import { file, resetStore } from '../helpers';
describe('RepoPreview', () => {
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
index 7d2174196c9..507bca983df 100644
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ b/spec/javascripts/repo/components/repo_tab_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoTab from '~/repo/components/repo_tab.vue';
+import store from '~/ide/stores';
+import repoTab from '~/ide/components/repo_tab.vue';
import { file, resetStore } from '../helpers';
describe('RepoTab', () => {
@@ -31,16 +31,16 @@ describe('RepoTab', () => {
expect(name.textContent.trim()).toEqual(vm.tab.name);
});
- it('calls setFileActive when clicking tab', () => {
+ it('fires clickFile when the link is clicked', () => {
vm = createComponent({
tab: file(),
});
- spyOn(vm, 'setFileActive');
+ spyOn(vm, 'clickFile');
vm.$el.click();
- expect(vm.setFileActive).toHaveBeenCalledWith(vm.tab);
+ expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
});
it('calls closeFile when clicking close button', () => {
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
index 1fb2242c051..0beaf643793 100644
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ b/spec/javascripts/repo/components/repo_tabs_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import repoTabs from '~/repo/components/repo_tabs.vue';
+import store from '~/ide/stores';
+import repoTabs from '~/ide/components/repo_tabs.vue';
import { file, resetStore } from '../helpers';
describe('RepoTabs', () => {
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
index 820a44992b4..ac43d221198 100644
--- a/spec/javascripts/repo/helpers.js
+++ b/spec/javascripts/repo/helpers.js
@@ -1,5 +1,5 @@
-import { decorateData } from '~/repo/stores/utils';
-import state from '~/repo/stores/state';
+import { decorateData } from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
export const resetStore = (store) => {
store.replaceState(state());
@@ -12,4 +12,5 @@ export const file = (name = 'name', id = name, type = '') => decorateData({
url: 'url',
name,
path: name,
+ lastCommit: {},
});
diff --git a/spec/javascripts/repo/lib/common/disposable_spec.js b/spec/javascripts/repo/lib/common/disposable_spec.js
index 62c3913bf4d..af12ca15369 100644
--- a/spec/javascripts/repo/lib/common/disposable_spec.js
+++ b/spec/javascripts/repo/lib/common/disposable_spec.js
@@ -1,4 +1,4 @@
-import Disposable from '~/repo/lib/common/disposable';
+import Disposable from '~/ide/lib/common/disposable';
describe('Multi-file editor library disposable class', () => {
let instance;
diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js
index 8c134f178c0..563c2e33834 100644
--- a/spec/javascripts/repo/lib/common/model_manager_spec.js
+++ b/spec/javascripts/repo/lib/common/model_manager_spec.js
@@ -1,6 +1,6 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import ModelManager from '~/repo/lib/common/model_manager';
+import monacoLoader from '~/ide/monaco_loader';
+import ModelManager from '~/ide/lib/common/model_manager';
import { file } from '../../helpers';
describe('Multi-file editor library model manager', () => {
diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js
index d41ade237ca..878a4a3f3fe 100644
--- a/spec/javascripts/repo/lib/common/model_spec.js
+++ b/spec/javascripts/repo/lib/common/model_spec.js
@@ -1,6 +1,6 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import Model from '~/repo/lib/common/model';
+import monacoLoader from '~/ide/monaco_loader';
+import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library model', () => {
diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js
index 2e32e8fa0bd..fea12d74dca 100644
--- a/spec/javascripts/repo/lib/decorations/controller_spec.js
+++ b/spec/javascripts/repo/lib/decorations/controller_spec.js
@@ -1,8 +1,8 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import editor from '~/repo/lib/editor';
-import DecorationsController from '~/repo/lib/decorations/controller';
-import Model from '~/repo/lib/common/model';
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import Model from '~/ide/lib/common/model';
import { file } from '../../helpers';
describe('Multi-file editor library decorations controller', () => {
diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js
index ed62e28d3a3..1d55c165260 100644
--- a/spec/javascripts/repo/lib/diff/controller_spec.js
+++ b/spec/javascripts/repo/lib/diff/controller_spec.js
@@ -1,10 +1,10 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import editor from '~/repo/lib/editor';
-import ModelManager from '~/repo/lib/common/model_manager';
-import DecorationsController from '~/repo/lib/decorations/controller';
-import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/repo/lib/diff/controller';
-import { computeDiff } from '~/repo/lib/diff/diff';
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
+import ModelManager from '~/ide/lib/common/model_manager';
+import DecorationsController from '~/ide/lib/decorations/controller';
+import DirtyDiffController, { getDiffChangeType, getDecorator } from '~/ide/lib/diff/controller';
+import { computeDiff } from '~/ide/lib/diff/diff';
import { file } from '../../helpers';
describe('Multi-file editor library dirty diff controller', () => {
diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js
index 3269ec5d2c9..57f3ac3d365 100644
--- a/spec/javascripts/repo/lib/diff/diff_spec.js
+++ b/spec/javascripts/repo/lib/diff/diff_spec.js
@@ -1,4 +1,4 @@
-import { computeDiff } from '~/repo/lib/diff/diff';
+import { computeDiff } from '~/ide/lib/diff/diff';
describe('Multi-file editor library diff calculator', () => {
describe('computeDiff', () => {
diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js
index b4887d063ed..edbf5450dce 100644
--- a/spec/javascripts/repo/lib/editor_options_spec.js
+++ b/spec/javascripts/repo/lib/editor_options_spec.js
@@ -1,4 +1,4 @@
-import editorOptions from '~/repo/lib/editor_options';
+import editorOptions from '~/ide/lib/editor_options';
describe('Multi-file editor library editor options', () => {
it('returns an array', () => {
diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js
index cd32832a232..8d51d48a782 100644
--- a/spec/javascripts/repo/lib/editor_spec.js
+++ b/spec/javascripts/repo/lib/editor_spec.js
@@ -1,6 +1,6 @@
/* global monaco */
-import monacoLoader from '~/repo/monaco_loader';
-import editor from '~/repo/lib/editor';
+import monacoLoader from '~/ide/monaco_loader';
+import editor from '~/ide/lib/editor';
import { file } from '../helpers';
describe('Multi-file editor library', () => {
diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js
index 887a80160fc..b8ac36972aa 100644
--- a/spec/javascripts/repo/monaco_loader_spec.js
+++ b/spec/javascripts/repo/monaco_loader_spec.js
@@ -1,5 +1,5 @@
import monacoContext from 'monaco-editor/dev/vs/loader';
-import monacoLoader from '~/repo/monaco_loader';
+import monacoLoader from '~/ide/monaco_loader';
describe('MonacoLoader', () => {
it('calls require.config and exports require', () => {
diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js
index af9d6835a67..00d16fd790d 100644
--- a/spec/javascripts/repo/stores/actions/branch_spec.js
+++ b/spec/javascripts/repo/stores/actions/branch_spec.js
@@ -1,5 +1,5 @@
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { resetStore } from '../../helpers';
describe('Multi-file store branch actions', () => {
@@ -16,19 +16,25 @@ describe('Multi-file store branch actions', () => {
}));
spyOn(history, 'pushState');
- store.state.project.id = 2;
- store.state.currentBranch = 'testing';
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'testing';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
});
it('creates new branch', (done) => {
store.dispatch('createNewBranch', 'master')
.then(() => {
- expect(store.state.currentBranch).toBe('testing');
- expect(service.createBranch).toHaveBeenCalledWith(2, {
+ expect(store.state.currentBranchId).toBe('testing');
+ expect(service.createBranch).toHaveBeenCalledWith('abcproject', {
branch: 'master',
ref: 'testing',
});
- expect(history.pushState).toHaveBeenCalled();
done();
})
diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js
index 099c0556e71..8ce01d3bf12 100644
--- a/spec/javascripts/repo/stores/actions/file_spec.js
+++ b/spec/javascripts/repo/stores/actions/file_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store file actions', () => {
@@ -24,8 +24,6 @@ describe('Multi-file store file actions', () => {
localFile.parentTreeUrl = 'parentTreeUrl';
store.state.openFiles.push(localFile);
-
- spyOn(history, 'pushState');
});
afterEach(() => {
@@ -82,15 +80,6 @@ describe('Multi-file store file actions', () => {
}).catch(done.fail);
});
- it('calls pushState when no open files are left', (done) => {
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'parentTreeUrl');
-
- done();
- }).catch(done.fail);
- });
-
it('sets next file as active', (done) => {
const f = file();
store.state.openFiles.push(f);
@@ -322,8 +311,26 @@ describe('Multi-file store file actions', () => {
});
describe('createTempFile', () => {
+ let projectTree;
+
beforeEach(() => {
document.body.innerHTML += '<div class="flash-container"></div>';
+
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
});
afterEach(() => {
@@ -332,11 +339,13 @@ describe('Multi-file store file actions', () => {
it('creates temp file', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(f.tempFile).toBeTruthy();
- expect(store.state.tree.length).toBe(1);
+ expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
done();
}).catch(done.fail);
@@ -344,8 +353,10 @@ describe('Multi-file store file actions', () => {
it('adds tmp file to open files', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(store.state.openFiles.length).toBe(1);
expect(store.state.openFiles[0].name).toBe(f.name);
@@ -356,8 +367,10 @@ describe('Multi-file store file actions', () => {
it('sets tmp file as active', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(f.active).toBeTruthy();
@@ -367,8 +380,10 @@ describe('Multi-file store file actions', () => {
it('enters edit mode if file is not base64', (done) => {
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then(() => {
expect(store.state.editMode).toBeTruthy();
@@ -376,24 +391,14 @@ describe('Multi-file store file actions', () => {
}).catch(done.fail);
});
- it('does not enter edit mode if file is base64', (done) => {
- store.dispatch('createTempFile', {
- tree: store.state,
- name: 'test',
- base64: true,
- }).then(() => {
- expect(store.state.editMode).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
it('creates flash message is file already exists', (done) => {
- store.state.tree.push(file('test', '1', 'blob'));
+ store.state.trees['abcproject/mybranch'].tree.push(file('test', '1', 'blob'));
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then(() => {
expect(document.querySelector('.flash-alert')).not.toBeNull();
@@ -402,11 +407,13 @@ describe('Multi-file store file actions', () => {
});
it('increases level of file', (done) => {
- store.state.level = 1;
+ store.state.trees['abcproject/mybranch'].level = 1;
store.dispatch('createTempFile', {
- tree: store.state,
name: 'test',
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
}).then((f) => {
expect(f.level).toBe(2);
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
index 2bbc49d5a9f..65351dbb7d9 100644
--- a/spec/javascripts/repo/stores/actions/tree_spec.js
+++ b/spec/javascripts/repo/stores/actions/tree_spec.js
@@ -1,10 +1,30 @@
import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { file, resetStore } from '../../helpers';
describe('Multi-file store tree actions', () => {
+ let projectTree;
+
+ const basicCallParameters = {
+ endpoint: 'rootEndpoint',
+ projectId: 'abcproject',
+ branch: 'master',
+ };
+
+ beforeEach(() => {
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: '',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+ });
+
afterEach(() => {
resetStore(store);
});
@@ -24,38 +44,32 @@ describe('Multi-file store tree actions', () => {
submodules: [{ name: 'submodule' }],
}),
}));
- spyOn(history, 'pushState');
-
- Object.assign(store.state.endpoints, {
- rootEndpoint: 'rootEndpoint',
- });
});
it('calls service getTreeData', (done) => {
- store.dispatch('getTreeData')
- .then(() => {
- expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
+ store.dispatch('getTreeData', basicCallParameters)
+ .then(() => {
+ expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
it('adds data into tree', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
- expect(store.state.tree.length).toBe(3);
- expect(store.state.tree[0].type).toBe('tree');
- expect(store.state.tree[1].type).toBe('submodule');
- expect(store.state.tree[2].type).toBe('blob');
+ projectTree = store.state.trees['abcproject/master'];
+ expect(projectTree.tree.length).toBe(3);
+ expect(projectTree.tree[0].type).toBe('tree');
+ expect(projectTree.tree[1].type).toBe('submodule');
+ expect(projectTree.tree[2].type).toBe('blob');
done();
}).catch(done.fail);
});
it('sets parent tree URL', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.parentTreeUrl).toBe('parent_tree_url');
@@ -64,10 +78,9 @@ describe('Multi-file store tree actions', () => {
});
it('sets last commit path', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
- expect(store.state.lastCommitPath).toBe('last_commit_path');
+ expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path');
done();
}).catch(done.fail);
@@ -76,8 +89,7 @@ describe('Multi-file store tree actions', () => {
it('sets root if not currently at root', (done) => {
store.state.isInitialRoot = false;
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(store.state.isInitialRoot).toBeTruthy();
expect(store.state.isRoot).toBeTruthy();
@@ -87,7 +99,7 @@ describe('Multi-file store tree actions', () => {
});
it('sets page title', (done) => {
- store.dispatch('getTreeData')
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
expect(document.title).toBe('test');
@@ -95,40 +107,15 @@ describe('Multi-file store tree actions', () => {
}).catch(done.fail);
});
- it('toggles loading', (done) => {
- store.dispatch('getTreeData')
- .then(() => {
- expect(store.state.loading).toBeTruthy();
-
- return Vue.nextTick();
- })
- .then(() => {
- expect(store.state.loading).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
- it('calls pushState with endpoint', (done) => {
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
- .then(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'rootEndpoint');
-
- done();
- }).catch(done.fail);
- });
-
it('calls getLastCommitData if prevLastCommitPath is not null', (done) => {
const getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
const oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
store.state.prevLastCommitPath = 'test';
- store.dispatch('getTreeData')
- .then(Vue.nextTick)
+ store.dispatch('getTreeData', basicCallParameters)
.then(() => {
- expect(getLastCommitDataSpy).toHaveBeenCalledWith(store.state);
+ expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree);
store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
@@ -149,6 +136,8 @@ describe('Multi-file store tree actions', () => {
store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
tree = {
+ projectId: 'abcproject',
+ branchId: 'master',
opened: false,
tree: [],
};
@@ -175,10 +164,11 @@ describe('Multi-file store tree actions', () => {
tree,
}).then(() => {
expect(getTreeDataSpy).toHaveBeenCalledWith({
+ projectId: 'abcproject',
+ branch: 'master',
endpoint: 'test',
tree,
});
- expect(store.state.previousUrl).toBe('test');
done();
}).catch(done.fail);
@@ -199,155 +189,29 @@ describe('Multi-file store tree actions', () => {
done();
}).catch(done.fail);
});
-
- it('pushes new state', (done) => {
- spyOn(history, 'pushState');
- Object.assign(tree, {
- opened: true,
- parentTreeUrl: 'testing',
- });
-
- store.dispatch('toggleTreeOpen', {
- endpoint: 'test',
- tree,
- }).then(() => {
- expect(history.pushState).toHaveBeenCalledWith(jasmine.anything(), '', 'testing');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('clickedTreeRow', () => {
- describe('tree', () => {
- let toggleTreeOpenSpy;
- let oldToggleTreeOpen;
-
- beforeEach(() => {
- toggleTreeOpenSpy = jasmine.createSpy('toggleTreeOpen');
-
- oldToggleTreeOpen = store._actions.toggleTreeOpen; // eslint-disable-line
- store._actions.toggleTreeOpen = [toggleTreeOpenSpy]; // eslint-disable-line
- });
-
- afterEach(() => {
- store._actions.toggleTreeOpen = oldToggleTreeOpen; // eslint-disable-line
- });
-
- it('opens tree', (done) => {
- const tree = {
- url: 'a',
- type: 'tree',
- };
-
- store.dispatch('clickedTreeRow', tree)
- .then(() => {
- expect(toggleTreeOpenSpy).toHaveBeenCalledWith({
- endpoint: tree.url,
- tree,
- });
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('submodule', () => {
- let row;
-
- beforeEach(() => {
- spyOn(urlUtils, 'visitUrl');
-
- row = {
- url: 'submoduleurl',
- type: 'submodule',
- loading: false,
- };
- });
-
- it('toggles loading for row', (done) => {
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(row.loading).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('opens submodule URL', (done) => {
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('submoduleurl');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('blob', () => {
- let row;
-
- beforeEach(() => {
- row = {
- type: 'blob',
- opened: false,
- };
- });
-
- it('calls getFileData', (done) => {
- const getFileDataSpy = jasmine.createSpy('getFileData');
- const oldGetFileData = store._actions.getFileData; // eslint-disable-line
- store._actions.getFileData = [getFileDataSpy]; // eslint-disable-line
-
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(getFileDataSpy).toHaveBeenCalledWith(row);
-
- store._actions.getFileData = oldGetFileData; // eslint-disable-line
-
- done();
- }).catch(done.fail);
- });
-
- it('calls setFileActive when file is opened', (done) => {
- const setFileActiveSpy = jasmine.createSpy('setFileActive');
- const oldSetFileActive = store._actions.setFileActive; // eslint-disable-line
- store._actions.setFileActive = [setFileActiveSpy]; // eslint-disable-line
-
- row.opened = true;
-
- store.dispatch('clickedTreeRow', row)
- .then(() => {
- expect(setFileActiveSpy).toHaveBeenCalledWith(row);
-
- store._actions.setFileActive = oldSetFileActive; // eslint-disable-line
-
- done();
- }).catch(done.fail);
- });
- });
});
describe('createTempTree', () => {
- it('creates temp tree', (done) => {
- store.dispatch('createTempTree', 'test')
- .then(() => {
- expect(store.state.tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].name).toBe('test');
- expect(store.state.tree[0].type).toBe('tree');
-
- done();
- }).catch(done.fail);
+ beforeEach(() => {
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ projectTree = store.state.trees['abcproject/mybranch'];
});
- it('creates .gitkeep file in temp tree', (done) => {
- store.dispatch('createTempTree', 'test')
- .then(() => {
- expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].tree[0].name).toBe('.gitkeep');
+ it('creates temp tree', (done) => {
+ store.dispatch('createTempTree', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ name: 'test',
+ parent: projectTree,
+ })
+ .then(() => {
+ expect(projectTree.tree[0].name).toBe('test');
+ expect(projectTree.tree[0].type).toBe('tree');
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
it('creates new folder inside another tree', (done) => {
@@ -357,35 +221,46 @@ describe('Multi-file store tree actions', () => {
tree: [],
};
- store.state.tree.push(tree);
+ projectTree.tree.push(tree);
- store.dispatch('createTempTree', 'testing/test')
- .then(() => {
- expect(store.state.tree[0].name).toBe('testing');
- expect(store.state.tree[0].tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].tree[0].name).toBe('test');
- expect(store.state.tree[0].tree[0].type).toBe('tree');
+ store.dispatch('createTempTree', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ parent: projectTree,
+ })
+ .then(() => {
+ expect(projectTree.tree[0].name).toBe('testing');
+ expect(projectTree.tree[0].tree[0].tempFile).toBeTruthy();
+ expect(projectTree.tree[0].tree[0].name).toBe('test');
+ expect(projectTree.tree[0].tree[0].type).toBe('tree');
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
it('does not create new tree if already exists', (done) => {
const tree = {
type: 'tree',
name: 'testing',
+ endpoint: 'test',
tree: [],
};
- store.state.tree.push(tree);
+ projectTree.tree.push(tree);
- store.dispatch('createTempTree', 'testing/test')
- .then(() => {
- expect(store.state.tree[0].name).toBe('testing');
- expect(store.state.tree[0].tempFile).toBeUndefined();
+ store.dispatch('createTempTree', {
+ projectId: store.state.currentProjectId,
+ branchId: store.state.currentBranchId,
+ name: 'testing/test',
+ parent: projectTree,
+ })
+ .then(() => {
+ expect(projectTree.tree[0].name).toBe('testing');
+ expect(projectTree.tree[0].tempFile).toBeUndefined();
- done();
- }).catch(done.fail);
+ done();
+ }).catch(done.fail);
});
});
@@ -405,12 +280,17 @@ describe('Multi-file store tree actions', () => {
}]),
}));
- store.state.tree.push(file('testing', '1', 'tree'));
- store.state.lastCommitPath = 'lastcommitpath';
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+
+ projectTree = store.state.trees['abcproject/mybranch'];
+ projectTree.tree.push(file('testing', '1', 'tree'));
+ projectTree.lastCommitPath = 'lastcommitpath';
});
it('calls service with lastCommitPath', (done) => {
- store.dispatch('getLastCommitData')
+ store.dispatch('getLastCommitData', projectTree)
.then(() => {
expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
@@ -419,22 +299,22 @@ describe('Multi-file store tree actions', () => {
});
it('updates trees last commit data', (done) => {
- store.dispatch('getLastCommitData')
- .then(Vue.nextTick)
+ store.dispatch('getLastCommitData', projectTree)
+ .then(Vue.nextTick)
.then(() => {
- expect(store.state.tree[0].lastCommit.message).toBe('commit message');
+ expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
done();
}).catch(done.fail);
});
it('does not update entry if not found', (done) => {
- store.state.tree[0].name = 'a';
+ projectTree.tree[0].name = 'a';
- store.dispatch('getLastCommitData')
+ store.dispatch('getLastCommitData', projectTree)
.then(Vue.nextTick)
.then(() => {
- expect(store.state.tree[0].lastCommit.message).not.toBe('commit message');
+ expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
done();
}).catch(done.fail);
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
index 21d87e46216..0b0d34f072a 100644
--- a/spec/javascripts/repo/stores/actions_spec.js
+++ b/spec/javascripts/repo/stores/actions_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import * as urlUtils from '~/lib/utils/url_utility';
-import store from '~/repo/stores';
-import service from '~/repo/services';
+import store from '~/ide/stores';
+import service from '~/ide/services';
import { resetStore, file } from '../helpers';
describe('Multi-file store actions', () => {
@@ -110,6 +110,7 @@ describe('Multi-file store actions', () => {
it('can force closed if there are changed files', (done) => {
store.state.editMode = true;
+
store.state.openFiles.push(file());
store.state.openFiles[0].changed = true;
@@ -125,7 +126,6 @@ describe('Multi-file store actions', () => {
it('discards file changes', (done) => {
const f = file();
store.state.editMode = true;
- store.state.tree.push(f);
store.state.openFiles.push(f);
f.changed = true;
@@ -141,8 +141,6 @@ describe('Multi-file store actions', () => {
describe('toggleBlobView', () => {
it('sets edit mode view if in edit mode', (done) => {
- store.state.editMode = true;
-
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-editor');
@@ -153,6 +151,8 @@ describe('Multi-file store actions', () => {
});
it('sets preview mode view if not in edit mode', (done) => {
+ store.state.editMode = false;
+
store.dispatch('toggleBlobView')
.then(() => {
expect(store.state.currentBlobView).toBe('repo-preview');
@@ -165,9 +165,15 @@ describe('Multi-file store actions', () => {
describe('checkCommitStatus', () => {
beforeEach(() => {
- store.state.project.id = 2;
- store.state.currentBranch = 'master';
- store.state.currentRef = '1';
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
});
it('calls service', (done) => {
@@ -177,7 +183,7 @@ describe('Multi-file store actions', () => {
store.dispatch('checkCommitStatus')
.then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith(2, 'master');
+ expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
done();
})
@@ -221,7 +227,17 @@ describe('Multi-file store actions', () => {
document.body.innerHTML += '<div class="flash-container"></div>';
- store.state.project.id = 123;
+ store.state.currentProjectId = 'abcproject';
+ store.state.currentBranchId = 'master';
+ store.state.projects.abcproject = {
+ web_url: 'webUrl',
+ branches: {
+ master: {
+ workingReference: '1',
+ },
+ },
+ };
+
payload = {
branch: 'master',
};
@@ -248,7 +264,7 @@ describe('Multi-file store actions', () => {
it('calls service', (done) => {
store.dispatch('commitChanges', { payload, newMr: false })
.then(() => {
- expect(service.commit).toHaveBeenCalledWith(123, payload);
+ expect(service.commit).toHaveBeenCalledWith('abcproject', payload);
done();
}).catch(done.fail);
@@ -284,17 +300,6 @@ describe('Multi-file store actions', () => {
}).catch(done.fail);
});
- it('toggles edit mode', (done) => {
- store.state.editMode = true;
-
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(store.state.editMode).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
it('closes all files', (done) => {
store.state.openFiles.push(file());
store.state.openFiles[0].opened = true;
@@ -317,23 +322,12 @@ describe('Multi-file store actions', () => {
}).catch(done.fail);
});
- it('updates commit ref', (done) => {
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(store.state.currentRef).toBe('123456');
-
- done();
- }).catch(done.fail);
- });
-
it('redirects to new merge request page', (done) => {
spyOn(urlUtils, 'visitUrl');
- store.state.endpoints.newMergeRequestUrl = 'newMergeRequestUrl?branch=';
-
store.dispatch('commitChanges', { payload, newMr: true })
.then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('newMergeRequestUrl?branch=master');
+ expect(urlUtils.visitUrl).toHaveBeenCalledWith('webUrl/merge_requests/new?merge_request%5Bsource_branch%5D=master');
done();
}).catch(done.fail);
@@ -363,15 +357,30 @@ describe('Multi-file store actions', () => {
});
describe('createTempEntry', () => {
+ beforeEach(() => {
+ store.state.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ store.state.projects.abcproject = {
+ web_url: '',
+ };
+ });
+
it('creates a temp tree', (done) => {
+ const projectTree = store.state.trees['abcproject/mybranch'];
+
store.dispatch('createTempEntry', {
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
name: 'test',
type: 'tree',
})
.then(() => {
- expect(store.state.tree.length).toBe(1);
- expect(store.state.tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].type).toBe('tree');
+ const baseTree = projectTree.tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].tempFile).toBeTruthy();
+ expect(baseTree[0].type).toBe('tree');
done();
})
@@ -379,14 +388,20 @@ describe('Multi-file store actions', () => {
});
it('creates temp file', (done) => {
+ const projectTree = store.state.trees['abcproject/mybranch'];
+
store.dispatch('createTempEntry', {
+ projectId: 'abcproject',
+ branchId: 'mybranch',
+ parent: projectTree,
name: 'test',
type: 'blob',
})
.then(() => {
- expect(store.state.tree.length).toBe(1);
- expect(store.state.tree[0].tempFile).toBeTruthy();
- expect(store.state.tree[0].type).toBe('blob');
+ const baseTree = projectTree.tree;
+ expect(baseTree.length).toBe(1);
+ expect(baseTree[0].tempFile).toBeTruthy();
+ expect(baseTree[0].type).toBe('blob');
done();
})
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
index 952b8ec3a59..d0d5934f29a 100644
--- a/spec/javascripts/repo/stores/getters_spec.js
+++ b/spec/javascripts/repo/stores/getters_spec.js
@@ -1,5 +1,5 @@
-import * as getters from '~/repo/stores/getters';
-import state from '~/repo/stores/state';
+import * as getters from '~/ide/stores/getters';
+import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store getters', () => {
@@ -9,20 +9,6 @@ describe('Multi-file store getters', () => {
localState = state();
});
- describe('treeList', () => {
- it('returns flat tree list', () => {
- localState.tree.push(file('1'));
- localState.tree[0].tree.push(file('2'));
- localState.tree[0].tree[0].tree.push(file('3'));
-
- const treeList = getters.treeList(localState);
-
- expect(treeList.length).toBe(3);
- expect(treeList[1].name).toBe(localState.tree[0].tree[0].name);
- expect(treeList[2].name).toBe(localState.tree[0].tree[0].tree[0].name);
- });
- });
-
describe('changedFiles', () => {
it('returns a list of changed opened files', () => {
localState.openFiles.push(file());
@@ -49,7 +35,7 @@ describe('Multi-file store getters', () => {
localState.openFiles.push(file());
localState.openFiles.push(file('active'));
- expect(getters.activeFile(localState)).toBeUndefined();
+ expect(getters.activeFile(localState)).toBeNull();
});
});
@@ -67,18 +53,6 @@ describe('Multi-file store getters', () => {
});
});
- describe('isCollapsed', () => {
- it('returns true if state has open files', () => {
- localState.openFiles.push(file());
-
- expect(getters.isCollapsed(localState)).toBeTruthy();
- });
-
- it('returns false if state has no open files', () => {
- expect(getters.isCollapsed(localState)).toBeFalsy();
- });
- });
-
describe('canEditFile', () => {
beforeEach(() => {
localState.onTopOfBranch = true;
@@ -109,12 +83,6 @@ describe('Multi-file store getters', () => {
expect(getters.canEditFile(localState)).toBeFalsy();
});
-
- it('returns false if user can commit but on a branch', () => {
- localState.onTopOfBranch = false;
-
- expect(getters.canEditFile(localState)).toBeFalsy();
- });
});
describe('modifiedFiles', () => {
diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js
index 3c06794d5e3..a7167537ef2 100644
--- a/spec/javascripts/repo/stores/mutations/branch_spec.js
+++ b/spec/javascripts/repo/stores/mutations/branch_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations/branch';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations/branch';
+import state from '~/ide/stores/state';
describe('Multi-file store branch mutations', () => {
let localState;
@@ -12,7 +12,7 @@ describe('Multi-file store branch mutations', () => {
it('sets currentBranch', () => {
mutations.SET_CURRENT_BRANCH(localState, 'master');
- expect(localState.currentBranch).toBe('master');
+ expect(localState.currentBranchId).toBe('master');
});
});
});
diff --git a/spec/javascripts/repo/stores/mutations/file_spec.js b/spec/javascripts/repo/stores/mutations/file_spec.js
index 2f2835dde1f..947a60587df 100644
--- a/spec/javascripts/repo/stores/mutations/file_spec.js
+++ b/spec/javascripts/repo/stores/mutations/file_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations/file';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations/file';
+import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store file mutations', () => {
diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js
index 1c76cfed9c8..cf1248ba28b 100644
--- a/spec/javascripts/repo/stores/mutations/tree_spec.js
+++ b/spec/javascripts/repo/stores/mutations/tree_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations/tree';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations/tree';
+import state from '~/ide/stores/state';
import { file } from '../../helpers';
describe('Multi-file store tree mutations', () => {
diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js
index d1c9885e01d..5fd8ad94972 100644
--- a/spec/javascripts/repo/stores/mutations_spec.js
+++ b/spec/javascripts/repo/stores/mutations_spec.js
@@ -1,5 +1,5 @@
-import mutations from '~/repo/stores/mutations';
-import state from '~/repo/stores/state';
+import mutations from '~/ide/stores/mutations';
+import state from '~/ide/stores/state';
import { file } from '../helpers';
describe('Multi-file store mutations', () => {
@@ -65,11 +65,11 @@ describe('Multi-file store mutations', () => {
it('toggles editMode', () => {
mutations.TOGGLE_EDIT_MODE(localState);
- expect(localState.editMode).toBeTruthy();
+ expect(localState.editMode).toBeFalsy();
mutations.TOGGLE_EDIT_MODE(localState);
- expect(localState.editMode).toBeFalsy();
+ expect(localState.editMode).toBeTruthy();
});
});
@@ -85,14 +85,6 @@ describe('Multi-file store mutations', () => {
});
});
- describe('SET_COMMIT_REF', () => {
- it('sets currentRef', () => {
- mutations.SET_COMMIT_REF(localState, '123');
-
- expect(localState.currentRef).toBe('123');
- });
- });
-
describe('SET_ROOT', () => {
it('sets isRoot & initialRoot', () => {
mutations.SET_ROOT(localState, true);
@@ -107,11 +99,27 @@ describe('Multi-file store mutations', () => {
});
});
- describe('SET_PREVIOUS_URL', () => {
- it('sets previousUrl', () => {
- mutations.SET_PREVIOUS_URL(localState, 'testing');
+ describe('SET_LEFT_PANEL_COLLAPSED', () => {
+ it('sets left panel collapsed', () => {
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.leftPanelCollapsed).toBeTruthy();
+
+ mutations.SET_LEFT_PANEL_COLLAPSED(localState, false);
+
+ expect(localState.leftPanelCollapsed).toBeFalsy();
+ });
+ });
+
+ describe('SET_RIGHT_PANEL_COLLAPSED', () => {
+ it('sets right panel collapsed', () => {
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, true);
+
+ expect(localState.rightPanelCollapsed).toBeTruthy();
+
+ mutations.SET_RIGHT_PANEL_COLLAPSED(localState, false);
- expect(localState.previousUrl).toBe('testing');
+ expect(localState.rightPanelCollapsed).toBeFalsy();
});
});
});
diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js
index 37287c587d7..89745a2029e 100644
--- a/spec/javascripts/repo/stores/utils_spec.js
+++ b/spec/javascripts/repo/stores/utils_spec.js
@@ -1,4 +1,6 @@
-import * as utils from '~/repo/stores/utils';
+import * as utils from '~/ide/stores/utils';
+import state from '~/ide/stores/state';
+import { file } from '../helpers';
describe('Multi-file store utils', () => {
describe('setPageTitle', () => {
@@ -9,13 +11,28 @@ describe('Multi-file store utils', () => {
});
});
- describe('pushState', () => {
- it('calls history.pushState', () => {
- spyOn(history, 'pushState');
+ describe('treeList', () => {
+ let localState;
- utils.pushState('test');
+ beforeEach(() => {
+ localState = state();
+ });
+
+ it('returns flat tree list', () => {
+ localState.trees = [];
+ localState.trees['abcproject/mybranch'] = {
+ tree: [],
+ };
+ const baseTree = localState.trees['abcproject/mybranch'].tree;
+ baseTree.push(file('1'));
+ baseTree[0].tree.push(file('2'));
+ baseTree[0].tree[0].tree.push(file('3'));
+
+ const treeList = utils.treeList(localState, 'abcproject/mybranch');
- expect(history.pushState).toHaveBeenCalledWith({ url: 'test' }, '', 'test');
+ expect(treeList.length).toBe(3);
+ expect(treeList[1].name).toBe(baseTree[0].tree[0].name);
+ expect(treeList[2].name).toBe(baseTree[0].tree[0].tree[0].name);
});
});
@@ -52,10 +69,10 @@ describe('Multi-file store utils', () => {
});
describe('findIndexOfFile', () => {
- let state;
+ let localState;
beforeEach(() => {
- state = [{
+ localState = [{
path: '1',
}, {
path: '2',
@@ -63,7 +80,7 @@ describe('Multi-file store utils', () => {
});
it('finds in the index of an entry by path', () => {
- const index = utils.findIndexOfFile(state, {
+ const index = utils.findIndexOfFile(localState, {
path: '2',
});
@@ -72,10 +89,10 @@ describe('Multi-file store utils', () => {
});
describe('findEntry', () => {
- let state;
+ let localState;
beforeEach(() => {
- state = {
+ localState = {
tree: [{
type: 'tree',
name: 'test',
@@ -87,14 +104,14 @@ describe('Multi-file store utils', () => {
});
it('returns an entry found by name', () => {
- const foundEntry = utils.findEntry(state, 'tree', 'test');
+ const foundEntry = utils.findEntry(localState.tree, 'tree', 'test');
expect(foundEntry.type).toBe('tree');
expect(foundEntry.name).toBe('test');
});
it('returns undefined when no entry found', () => {
- const foundEntry = utils.findEntry(state, 'blob', 'test');
+ const foundEntry = utils.findEntry(localState.tree, 'blob', 'test');
expect(foundEntry).toBeUndefined();
});
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 9e6d0aa472c..74b343c573e 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import mrWidgetOptions from '~/vue_merge_request_widget/mr_widget_options';
import eventHub from '~/vue_merge_request_widget/event_hub';
import notify from '~/lib/utils/notify';
+import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from './mock_data';
import mountComponent from '../helpers/vue_mount_component_helper';
@@ -344,4 +345,31 @@ describe('mrWidgetOptions', () => {
expect(comps['mr-widget-merge-when-pipeline-succeeds']).toBeDefined();
});
});
+
+ describe('rendering relatedLinks', () => {
+ beforeEach((done) => {
+ vm.mr.relatedLinks = {
+ assignToMe: null,
+ closing: `
+ <a class="close-related-link" href="#'>
+ Close
+ </a>
+ `,
+ mentioned: '',
+ };
+ Vue.nextTick(done);
+ });
+
+ it('renders if there are relatedLinks', () => {
+ expect(vm.$el.querySelector('.close-related-link')).toBeDefined();
+ });
+
+ it('does not render if state is nothingToMerge', (done) => {
+ vm.mr.state = stateKey.nothingToMerge;
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.close-related-link')).toBeNull();
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
index 8e5614b20f0..33d052aceb2 100644
--- a/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
+++ b/spec/javascripts/vue_mr_widget/stores/mr_widget_store_spec.js
@@ -1,4 +1,5 @@
import MergeRequestStore from '~/vue_merge_request_widget/stores/mr_widget_store';
+import { stateKey } from '~/vue_merge_request_widget/stores/state_maps';
import mockData from '../mock_data';
describe('MergeRequestStore', () => {
@@ -52,5 +53,17 @@ describe('MergeRequestStore', () => {
expect(store.isPipelineSkipped).toBe(false);
});
});
+
+ describe('isNothingToMergeState', () => {
+ it('returns true when nothingToMerge', () => {
+ store.state = stateKey.nothingToMerge;
+ expect(store.isNothingToMergeState).toEqual(true);
+ });
+
+ it('returns false when not nothingToMerge', () => {
+ store.state = 'state';
+ expect(store.isNothingToMergeState).toEqual(false);
+ });
+ });
});
});
diff --git a/spec/lib/gitlab/git/gitlab_projects_spec.rb b/spec/lib/gitlab/git/gitlab_projects_spec.rb
index 18906955df6..24da9589458 100644
--- a/spec/lib/gitlab/git/gitlab_projects_spec.rb
+++ b/spec/lib/gitlab/git/gitlab_projects_spec.rb
@@ -41,7 +41,8 @@ describe Gitlab::Git::GitlabProjects do
end
it "fails if the source path doesn't exist" do
- expect(logger).to receive(:error).with("mv-project failed: source path <#{tmp_repos_path}/bad-src.git> does not exist.")
+ expected_source_path = File.join(tmp_repos_path, 'bad-src.git')
+ expect(logger).to receive(:error).with("mv-project failed: source path <#{expected_source_path}> does not exist.")
result = build_gitlab_projects(tmp_repos_path, 'bad-src.git').mv_project('repo.git')
expect(result).to be_falsy
@@ -50,7 +51,8 @@ describe Gitlab::Git::GitlabProjects do
it 'fails if the destination path already exists' do
FileUtils.mkdir_p(File.join(tmp_repos_path, 'already-exists.git'))
- message = "mv-project failed: destination path <#{tmp_repos_path}/already-exists.git> already exists."
+ expected_distination_path = File.join(tmp_repos_path, 'already-exists.git')
+ message = "mv-project failed: destination path <#{expected_distination_path}> already exists."
expect(logger).to receive(:error).with(message)
expect(gl_projects.mv_project('already-exists.git')).to be_falsy
diff --git a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
index 168e5d07504..46a57e08963 100644
--- a/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/repository_importer_spec.rb
@@ -70,7 +70,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
describe '#execute' do
it 'imports the repository and wiki' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(true)
@@ -93,7 +93,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'does not import the repository if it already exists' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(false)
@@ -115,7 +115,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'does not import the wiki if it is disabled' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(true)
@@ -137,7 +137,7 @@ describe Gitlab::GithubImport::Importer::RepositoryImporter do
end
it 'does not import the wiki if the repository could not be imported' do
- expect(repository)
+ expect(project)
.to receive(:empty_repo?)
.and_return(true)
diff --git a/spec/models/blob_viewer/package_json_spec.rb b/spec/models/blob_viewer/package_json_spec.rb
index 0f8330e91c1..5ed2f4400bc 100644
--- a/spec/models/blob_viewer/package_json_spec.rb
+++ b/spec/models/blob_viewer/package_json_spec.rb
@@ -22,4 +22,51 @@ describe BlobViewer::PackageJson do
expect(subject.package_name).to eq('module-name')
end
end
+
+ describe '#package_url' do
+ it 'returns the package URL' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq("https://www.npmjs.com/package/#{subject.package_name}")
+ end
+ end
+
+ describe '#package_type' do
+ it 'returns "package"' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_type).to eq('package')
+ end
+ end
+
+ context 'when package.json has "private": true' do
+ let(:data) do
+ <<-SPEC.strip_heredoc
+ {
+ "name": "module-name",
+ "version": "10.3.1",
+ "private": true,
+ "homepage": "myawesomepackage.com"
+ }
+ SPEC
+ end
+ let(:blob) { fake_blob(path: 'package.json', data: data) }
+ subject { described_class.new(blob) }
+
+ describe '#package_url' do
+ it 'returns homepage if any' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_url).to eq('myawesomepackage.com')
+ end
+ end
+
+ describe '#package_type' do
+ it 'returns "private package"' do
+ expect(subject).to receive(:prepare!)
+
+ expect(subject.package_type).to eq('private package')
+ end
+ end
+ end
end
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index d18a5c9dfa6..cd955a5eb69 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -13,6 +13,45 @@ describe Commit do
it { is_expected.to include_module(StaticModel) }
end
+ describe '.lazy' do
+ set(:project) { create(:project, :repository) }
+
+ context 'when the commits are found' do
+ let(:oids) do
+ %w(
+ 498214de67004b1da3d820901307bed2a68a8ef6
+ c642fe9b8b9f28f9225d7ea953fe14e74748d53b
+ 6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9
+ 048721d90c449b244b7b4c53a9186b04330174ec
+ 281d3a76f31c812dbf48abce82ccf6860adedd81
+ )
+ end
+
+ subject { oids.map { |oid| described_class.lazy(project, oid) } }
+
+ it 'batches requests for commits' do
+ expect(project.repository).to receive(:commits_by).once.and_call_original
+
+ subject.first.title
+ subject.last.title
+ end
+
+ it 'maintains ordering' do
+ subject.each_with_index do |commit, i|
+ expect(commit.id).to eq(oids[i])
+ end
+ end
+ end
+
+ context 'when not found' do
+ it 'returns nil as commit' do
+ commit = described_class.lazy(project, 'deadbeef').__sync
+
+ expect(commit).to be_nil
+ end
+ end
+ end
+
describe '#author' do
it 'looks up the author in a case-insensitive way' do
user = create(:user, email: commit.author_email.upcase)
diff --git a/spec/models/concerns/blocks_json_serialization_spec.rb b/spec/models/concerns/blocks_json_serialization_spec.rb
new file mode 100644
index 00000000000..5906b588d0e
--- /dev/null
+++ b/spec/models/concerns/blocks_json_serialization_spec.rb
@@ -0,0 +1,17 @@
+require 'rails_helper'
+
+describe BlocksJsonSerialization do
+ DummyModel = Class.new do
+ include BlocksJsonSerialization
+ end
+
+ it 'blocks as_json' do
+ expect { DummyModel.new.as_json }
+ .to raise_error(described_class::JsonSerializationError, /DummyModel/)
+ end
+
+ it 'blocks to_json' do
+ expect { DummyModel.new.to_json }
+ .to raise_error(described_class::JsonSerializationError, /DummyModel/)
+ end
+end
diff --git a/spec/models/concerns/issuable_spec.rb b/spec/models/concerns/issuable_spec.rb
index 9df26f06a11..4b217df2e8f 100644
--- a/spec/models/concerns/issuable_spec.rb
+++ b/spec/models/concerns/issuable_spec.rb
@@ -291,7 +291,7 @@ describe Issuable do
context 'total_time_spent is updated' do
before do
- issue.spend_time(duration: 2, user: user, spent_at: Time.now)
+ issue.spend_time(duration: 2, user_id: user.id, spent_at: Time.now)
issue.save
expect(Gitlab::HookData::IssuableBuilder)
.to receive(:new).with(issue).and_return(builder)
@@ -485,7 +485,7 @@ describe Issuable do
let(:issue) { create(:issue) }
def spend_time(seconds)
- issue.spend_time(duration: seconds, user: user)
+ issue.spend_time(duration: seconds, user_id: user.id)
issue.save!
end
diff --git a/spec/models/concerns/milestoneish_spec.rb b/spec/models/concerns/milestoneish_spec.rb
index 9048da0c73d..673c609f534 100644
--- a/spec/models/concerns/milestoneish_spec.rb
+++ b/spec/models/concerns/milestoneish_spec.rb
@@ -189,9 +189,9 @@ describe Milestone, 'Milestoneish' do
describe '#total_issue_time_spent' do
it 'calculates total issue time spent' do
- closed_issue_1.spend_time(duration: 300, user: author)
+ closed_issue_1.spend_time(duration: 300, user_id: author.id)
closed_issue_1.save!
- closed_issue_2.spend_time(duration: 600, user: assignee)
+ closed_issue_2.spend_time(duration: 600, user_id: assignee.id)
closed_issue_2.save!
expect(milestone.total_issue_time_spent).to eq(900)
diff --git a/spec/models/identity_spec.rb b/spec/models/identity_spec.rb
index a45a6088831..7c66c98231b 100644
--- a/spec/models/identity_spec.rb
+++ b/spec/models/identity_spec.rb
@@ -44,4 +44,31 @@ describe Identity do
end
end
end
+
+ context 'callbacks' do
+ context 'before_save' do
+ describe 'normalizes extern uid' do
+ let!(:ldap_identity) { create(:identity, provider: 'ldapmain', extern_uid: 'uid=john smith,ou=people,dc=example,dc=com') }
+
+ it 'if extern_uid changes' do
+ expect(ldap_identity).not_to receive(:ensure_normalized_extern_uid)
+ ldap_identity.save
+ end
+
+ it 'if current_uid is nil' do
+ expect(ldap_identity).to receive(:ensure_normalized_extern_uid)
+
+ ldap_identity.update(extern_uid: nil)
+
+ expect(ldap_identity.extern_uid).to be_nil
+ end
+
+ it 'if extern_uid changed and not nil' do
+ ldap_identity.update(extern_uid: 'uid=john1,ou=PEOPLE,dc=example,dc=com')
+
+ expect(ldap_identity.extern_uid).to eq 'uid=john1,ou=people,dc=example,dc=com'
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index f805f2dcddb..cbeac2f05d3 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1863,11 +1863,10 @@ describe Project do
project.change_head(project.default_branch)
end
- it 'creates the new reference' do
- expect(project.repository.raw_repository).to receive(:write_ref).with('HEAD',
+ it 'creates the new reference with rugged' do
+ expect(project.repository.rugged.references).to receive(:create).with('HEAD',
"refs/heads/#{project.default_branch}",
force: true)
-
project.change_head(project.default_branch)
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index bdc430c9095..9a68ae086ea 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -239,6 +239,54 @@ describe Repository do
end
end
+ describe '#commits_by' do
+ set(:project) { create(:project, :repository) }
+
+ shared_examples 'batch commits fetching' do
+ let(:oids) { TestEnv::BRANCH_SHA.values }
+
+ subject { project.repository.commits_by(oids: oids) }
+
+ it 'finds each commit' do
+ expect(subject).not_to include(nil)
+ expect(subject.size).to eq(oids.size)
+ end
+
+ it 'returns only Commit instances' do
+ expect(subject).to all( be_a(Commit) )
+ end
+
+ context 'when some commits are not found ' do
+ let(:oids) do
+ ['deadbeef'] + TestEnv::BRANCH_SHA.values.first(10)
+ end
+
+ it 'returns only found commits' do
+ expect(subject).not_to include(nil)
+ expect(subject.size).to eq(10)
+ end
+ end
+
+ context 'when no oids are passed' do
+ let(:oids) { [] }
+
+ it 'does not call #batch_by_oid' do
+ expect(Gitlab::Git::Commit).not_to receive(:batch_by_oid)
+
+ subject
+ end
+ end
+ end
+
+ context 'when Gitaly list_commits_by_oid is enabled' do
+ it_behaves_like 'batch commits fetching'
+ end
+
+ context 'when Gitaly list_commits_by_oid is enabled', :disable_gitaly do
+ it_behaves_like 'batch commits fetching'
+ end
+ end
+
describe '#find_commits_by_message' do
shared_examples 'finding commits by message' do
it 'returns commits with messages containing a given string' do
@@ -1931,23 +1979,6 @@ describe Repository do
File.delete(path)
end
-
- it "attempting to call keep_around when exists a lock does not fail" do
- ref = repository.send(:keep_around_ref_name, sample_commit.id)
- path = File.join(repository.path, ref)
- lock_path = "#{path}.lock"
-
- FileUtils.mkdir_p(File.dirname(path))
- File.open(lock_path, 'w') { |f| f.write('') }
-
- begin
- expect { repository.keep_around(sample_commit.id) }.not_to raise_error(Gitlab::Git::Repository::GitError)
-
- expect(File.exist?(lock_path)).to be_falsey
- ensure
- File.delete(path)
- end
- end
end
describe '#update_ref' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 4687d9dfa00..e58e7588df0 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -12,6 +12,7 @@ describe User do
it { is_expected.to include_module(Referable) }
it { is_expected.to include_module(Sortable) }
it { is_expected.to include_module(TokenAuthenticatable) }
+ it { is_expected.to include_module(BlocksJsonSerialization) }
end
describe 'delegations' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 91616da6d9a..60dbd74d59d 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -150,6 +150,26 @@ describe API::MergeRequests do
expect(json_response.length).to eq(1)
expect(json_response.first['id']).to eq(merge_request3.id)
end
+
+ context 'search params' do
+ before do
+ merge_request.update(title: 'Search title', description: 'Search description')
+ end
+
+ it 'returns merge requests matching given search string for title' do
+ get api("/merge_requests", user), search: merge_request.title
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+
+ it 'returns merge requests for project matching given search string for description' do
+ get api("/merge_requests", user), project_id: project.id, search: merge_request.description
+
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request.id)
+ end
+ end
end
end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 88d347322a6..c38795ad1a1 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -1,6 +1,7 @@
require 'spec_helper'
describe PipelineSerializer do
+ set(:project) { create(:project, :repository) }
set(:user) { create(:user) }
let(:serializer) do
@@ -16,7 +17,7 @@ describe PipelineSerializer do
end
context 'when a single object is being serialized' do
- let(:resource) { create(:ci_empty_pipeline) }
+ let(:resource) { create(:ci_empty_pipeline, project: project) }
it 'serializers the pipeline object' do
expect(subject[:id]).to eq resource.id
@@ -24,7 +25,7 @@ describe PipelineSerializer do
end
context 'when multiple objects are being serialized' do
- let(:resource) { create_list(:ci_pipeline, 2) }
+ let(:resource) { create_list(:ci_pipeline, 2, project: project) }
it 'serializers the array of pipelines' do
expect(subject).not_to be_empty
@@ -100,7 +101,6 @@ describe PipelineSerializer do
context 'number of queries' do
let(:resource) { Ci::Pipeline.all }
- let(:project) { create(:project) }
before do
# Since RequestStore.active? is true we have to allow the
diff --git a/spec/services/files/delete_service_spec.rb b/spec/services/files/delete_service_spec.rb
new file mode 100644
index 00000000000..e9f8f0efe6b
--- /dev/null
+++ b/spec/services/files/delete_service_spec.rb
@@ -0,0 +1,64 @@
+require "spec_helper"
+
+describe Files::DeleteService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:file_path) { 'files/ruby/popen.rb' }
+ let(:branch_name) { project.default_branch }
+ let(:last_commit_sha) { nil }
+
+ let(:commit_params) do
+ {
+ file_path: file_path,
+ commit_message: "Delete File",
+ last_commit_sha: last_commit_sha,
+ start_project: project,
+ start_branch: project.default_branch,
+ branch_name: branch_name
+ }
+ end
+
+ shared_examples 'successfully deletes the file' do
+ it 'returns a hash with the :success status' do
+ results = subject.execute
+
+ expect(results[:status]).to match(:success)
+ end
+
+ it 'deletes the file' do
+ subject.execute
+
+ blob = project.repository.blob_at_branch(project.default_branch, file_path)
+
+ expect(blob).to be_nil
+ end
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe "#execute" do
+ context "when the file's last commit sha does not match the supplied last_commit_sha" do
+ let(:last_commit_sha) { "foo" }
+
+ it "returns a hash with the correct error message and a :error status " do
+ expect { subject.execute }
+ .to raise_error(Files::UpdateService::FileChangedError,
+ "You are attempting to delete a file that has been previously updated.")
+ end
+ end
+
+ context "when the file's last commit sha does match the supplied last_commit_sha" do
+ let(:last_commit_sha) { Gitlab::Git::Commit.last_for_path(project.repository, project.default_branch, file_path).sha }
+
+ it_behaves_like 'successfully deletes the file'
+ end
+
+ context "when the last_commit_sha is not supplied" do
+ it_behaves_like 'successfully deletes the file'
+ end
+ end
+end
diff --git a/spec/services/files/multi_service_spec.rb b/spec/services/files/multi_service_spec.rb
new file mode 100644
index 00000000000..085a28d267f
--- /dev/null
+++ b/spec/services/files/multi_service_spec.rb
@@ -0,0 +1,144 @@
+require "spec_helper"
+
+describe Files::MultiService do
+ subject { described_class.new(project, user, commit_params) }
+
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user) }
+ let(:branch_name) { project.default_branch }
+ let(:original_file_path) { 'files/ruby/popen.rb' }
+ let(:new_file_path) { 'files/ruby/popen.rb' }
+ let(:action) { 'update' }
+
+ let!(:original_commit_id) do
+ Gitlab::Git::Commit.last_for_path(project.repository, branch_name, original_file_path).sha
+ end
+
+ let(:actions) do
+ [
+ {
+ action: action,
+ file_path: new_file_path,
+ previous_path: original_file_path,
+ content: 'New content',
+ last_commit_id: original_commit_id
+ }
+ ]
+ end
+
+ let(:commit_params) do
+ {
+ commit_message: "Update File",
+ branch_name: branch_name,
+ start_branch: branch_name,
+ actions: actions
+ }
+ end
+
+ before do
+ project.team << [user, :master]
+ end
+
+ describe '#execute' do
+ context 'with a valid action' do
+ it 'returns a hash with the :success status ' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+
+ context 'with an invalid action' do
+ let(:action) { 'rename' }
+
+ it 'returns a hash with the :error status ' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(/Unknown action/)
+ end
+ end
+
+ describe 'Updating files' do
+ context 'when the file has been previously updated' do
+ before do
+ update_file(original_file_path)
+ end
+
+ it 'rejects the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(new_file_path)
+ end
+ end
+
+ context 'when the file have not been modified' do
+ it 'accepts the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:success)
+ end
+ end
+ end
+
+ context 'when moving a file' do
+ let(:action) { 'move' }
+ let(:new_file_path) { 'files/ruby/new_popen.rb' }
+
+ context 'when original file has been updated' do
+ before do
+ update_file(original_file_path)
+ end
+
+ it 'rejects the commit' do
+ results = subject.execute
+
+ expect(results[:status]).to eq(:error)
+ expect(results[:message]).to match(original_file_path)
+ end
+ end
+
+ context 'when original file have not been updated' do
+ it 'moves the file' do
+ results = subject.execute
+ blob = project.repository.blob_at_branch(branch_name, new_file_path)
+
+ expect(results[:status]).to eq(:success)
+ expect(blob).to be_present
+ end
+ end
+ end
+
+ context 'when file status validation is skipped' do
+ let(:action) { 'create' }
+ let(:new_file_path) { 'files/ruby/new_file.rb' }
+
+ it 'does not check the last commit' do
+ expect(Gitlab::Git::Commit).not_to receive(:last_for_path)
+
+ subject.execute
+ end
+
+ it 'creates the file' do
+ subject.execute
+
+ blob = project.repository.blob_at_branch(branch_name, new_file_path)
+
+ expect(blob).to be_present
+ end
+ end
+ end
+
+ def update_file(path)
+ params = {
+ file_path: path,
+ start_branch: branch_name,
+ branch_name: branch_name,
+ commit_message: 'Update file',
+ file_content: 'New content'
+ }
+
+ Files::UpdateService.new(project, user, params).execute
+ end
+end
diff --git a/spec/services/projects/unlink_fork_service_spec.rb b/spec/services/projects/unlink_fork_service_spec.rb
index 2bba71fef4f..3ec6139bfa6 100644
--- a/spec/services/projects/unlink_fork_service_spec.rb
+++ b/spec/services/projects/unlink_fork_service_spec.rb
@@ -62,6 +62,26 @@ describe Projects::UnlinkForkService do
expect(source.forks_count).to be_zero
end
+ context 'when the source has LFS objects' do
+ let(:lfs_object) { create(:lfs_object) }
+
+ before do
+ lfs_object.projects << project
+ end
+
+ it 'links the fork to the lfs object before unlinking' do
+ subject.execute
+
+ expect(lfs_object.projects).to include(forked_project)
+ end
+
+ it 'does not fail if the lfs objects were already linked' do
+ lfs_object.projects << forked_project
+
+ expect { subject.execute }.not_to raise_error
+ end
+ end
+
context 'when the original project was deleted' do
it 'does not fail when the original project is deleted' do
source = forked_project.forked_from_project
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index c35177f6ebc..eb46480fa54 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -209,7 +209,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: 3600,
- user: developer,
+ user_id: developer.id,
spent_at: DateTime.now.to_date
})
end
@@ -221,7 +221,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: -1800,
- user: developer,
+ user_id: developer.id,
spent_at: DateTime.now.to_date
})
end
@@ -233,7 +233,7 @@ describe QuickActions::InterpretService do
expect(updates).to eq(spend_time: {
duration: 1800,
- user: developer,
+ user_id: developer.id,
spent_at: Date.parse(date)
})
end
@@ -267,7 +267,7 @@ describe QuickActions::InterpretService do
it 'populates spend_time: :reset if content contains /remove_time_spent' do
_, updates = service.execute(content, issuable)
- expect(updates).to eq(spend_time: { duration: :reset, user: developer })
+ expect(updates).to eq(spend_time: { duration: :reset, user_id: developer.id })
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 47412110b4b..9025589ae0b 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -927,7 +927,7 @@ describe SystemNoteService do
# We need a custom noteable in order to the shared examples to be green.
let(:noteable) do
mr = create(:merge_request, source_project: project)
- mr.spend_time(duration: 360000, user: author)
+ mr.spend_time(duration: 360000, user_id: author.id)
mr.save!
mr
end
@@ -965,7 +965,7 @@ describe SystemNoteService do
end
def spend_time!(seconds)
- noteable.spend_time(duration: seconds, user: author)
+ noteable.spend_time(duration: seconds, user_id: author.id)
noteable.save!
end
end
diff --git a/spec/support/api/time_tracking_shared_examples.rb b/spec/support/api/time_tracking_shared_examples.rb
index af1083f4bfd..dd3089d22e5 100644
--- a/spec/support/api/time_tracking_shared_examples.rb
+++ b/spec/support/api/time_tracking_shared_examples.rb
@@ -79,7 +79,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1h'
@@ -91,7 +91,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
context 'when time to subtract is greater than the total spent time' do
it 'does not modify the total time spent' do
- issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
post api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/add_spent_time", user),
duration: '-1w'
@@ -119,7 +119,7 @@ shared_examples 'time tracking endpoints' do |issuable_name|
describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
it "returns the time stats for #{issuable_name}" do
- issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+ issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id },
time_estimate: 3600)
get api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.iid}/time_stats", user)
diff --git a/spec/support/api/v3/time_tracking_shared_examples.rb b/spec/support/api/v3/time_tracking_shared_examples.rb
index afe0f4cecda..f27a2d06c83 100644
--- a/spec/support/api/v3/time_tracking_shared_examples.rb
+++ b/spec/support/api/v3/time_tracking_shared_examples.rb
@@ -75,7 +75,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
context 'when subtracting time' do
it 'subtracts time of the total spent time' do
- issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '-1h'
@@ -87,7 +87,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
context 'when time to subtract is greater than the total spent time' do
it 'does not modify the total time spent' do
- issuable.update_attributes!(spend_time: { duration: 7200, user: user })
+ issuable.update_attributes!(spend_time: { duration: 7200, user_id: user.id })
post v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/add_spent_time", user),
duration: '-1w'
@@ -115,7 +115,7 @@ shared_examples 'V3 time tracking endpoints' do |issuable_name|
describe "GET /projects/:id/#{issuable_collection_name}/:#{issuable_name}_id/time_stats" do
it "returns the time stats for #{issuable_name}" do
- issuable.update_attributes!(spend_time: { duration: 1800, user: user },
+ issuable.update_attributes!(spend_time: { duration: 1800, user_id: user.id },
time_estimate: 3600)
get v3_api("/projects/#{project.id}/#{issuable_collection_name}/#{issuable.id}/time_stats", user)
diff --git a/spec/views/events/event/_push.html.haml_spec.rb b/spec/views/events/event/_push.html.haml_spec.rb
new file mode 100644
index 00000000000..f5634de4916
--- /dev/null
+++ b/spec/views/events/event/_push.html.haml_spec.rb
@@ -0,0 +1,55 @@
+require 'spec_helper'
+
+describe 'events/event/_push.html.haml' do
+ let(:event) { build_stubbed(:push_event) }
+
+ context 'with a branch' do
+ let(:payload) { build_stubbed(:push_event_payload, event: event) }
+
+ before do
+ allow(event).to receive(:push_event_payload).and_return(payload)
+ end
+
+ it 'links to the branch' do
+ allow(event.project.repository).to receive(:branch_exists?).with(event.ref_name).and_return(true)
+ link = project_commits_path(event.project, event.ref_name)
+
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to have_link(event.ref_name, href: link)
+ end
+
+ context 'that has been deleted' do
+ it 'does not link to the branch' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).not_to have_link(event.ref_name)
+ end
+ end
+ end
+
+ context 'with a tag' do
+ let(:payload) { build_stubbed(:push_event_payload, event: event, ref_type: :tag, ref: 'v0.1.0') }
+
+ before do
+ allow(event).to receive(:push_event_payload).and_return(payload)
+ end
+
+ it 'links to the tag' do
+ allow(event.project.repository).to receive(:tag_exists?).with(event.ref_name).and_return(true)
+ link = project_commits_path(event.project, event.ref_name)
+
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).to have_link(event.ref_name, href: link)
+ end
+
+ context 'that has been deleted' do
+ it 'does not link to the tag' do
+ render partial: 'events/event/push', locals: { event: event }
+
+ expect(rendered).not_to have_link(event.ref_name)
+ end
+ end
+ end
+end
diff --git a/spec/workers/concerns/project_import_options_spec.rb b/spec/workers/concerns/project_import_options_spec.rb
new file mode 100644
index 00000000000..b6c111df8b9
--- /dev/null
+++ b/spec/workers/concerns/project_import_options_spec.rb
@@ -0,0 +1,40 @@
+require 'spec_helper'
+
+describe ProjectImportOptions do
+ let(:project) { create(:project, :import_started) }
+ let(:job) { { 'args' => [project.id, nil, nil], 'jid' => '123' } }
+ let(:worker_class) do
+ Class.new do
+ include Sidekiq::Worker
+ include ProjectImportOptions
+ end
+ end
+
+ it 'sets default retry limit' do
+ expect(worker_class.sidekiq_options['retry']).to eq(ProjectImportOptions::IMPORT_RETRY_COUNT)
+ end
+
+ it 'sets default status expiration' do
+ expect(worker_class.sidekiq_options['status_expiration']).to eq(StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
+ end
+
+ describe '.sidekiq_retries_exhausted' do
+ it 'marks fork as failed' do
+ expect { worker_class.sidekiq_retries_exhausted_block.call(job) }.to change { project.reload.import_status }.from("started").to("failed")
+ end
+
+ it 'logs the appropriate error message for forked projects' do
+ allow_any_instance_of(Project).to receive(:forked?).and_return(true)
+
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(project.reload.import_error).to include("fork")
+ end
+
+ it 'logs the appropriate error message for forked projects' do
+ worker_class.sidekiq_retries_exhausted_block.call(job)
+
+ expect(project.reload.import_error).to include("import")
+ end
+ end
+end
diff --git a/spec/workers/repository_fork_worker_spec.rb b/spec/workers/repository_fork_worker_spec.rb
index 74c85848b7e..31598586f59 100644
--- a/spec/workers/repository_fork_worker_spec.rb
+++ b/spec/workers/repository_fork_worker_spec.rb
@@ -1,17 +1,21 @@
require 'spec_helper'
describe RepositoryForkWorker do
- let(:project) { create(:project, :repository) }
- let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
- let(:shell) { Gitlab::Shell.new }
-
- subject { described_class.new }
-
- before do
- allow(subject).to receive(:gitlab_shell).and_return(shell)
+ describe 'modules' do
+ it 'includes ProjectImportOptions' do
+ expect(described_class).to include_module(ProjectImportOptions)
+ end
end
describe "#perform" do
+ let(:project) { create(:project, :repository) }
+ let(:fork_project) { create(:project, :repository, :import_scheduled, forked_from_project: project) }
+ let(:shell) { Gitlab::Shell.new }
+
+ before do
+ allow(subject).to receive(:gitlab_shell).and_return(shell)
+ end
+
def perform!
subject.perform(fork_project.id, '/test/path', project.disk_path)
end
@@ -60,14 +64,7 @@ describe RepositoryForkWorker do
expect_fork_repository.and_return(false)
- expect { perform! }.to raise_error(RepositoryForkWorker::ForkError, error_message)
- end
-
- it 'handles unexpected error' do
- expect_fork_repository.and_raise(RuntimeError)
-
- expect { perform! }.to raise_error(RepositoryForkWorker::ForkError)
- expect(fork_project.reload.import_status).to eq('failed')
+ expect { perform! }.to raise_error(StandardError, error_message)
end
end
end
diff --git a/spec/workers/repository_import_worker_spec.rb b/spec/workers/repository_import_worker_spec.rb
index 0af537647ad..85ac14eb347 100644
--- a/spec/workers/repository_import_worker_spec.rb
+++ b/spec/workers/repository_import_worker_spec.rb
@@ -1,11 +1,15 @@
require 'spec_helper'
describe RepositoryImportWorker do
- let(:project) { create(:project, :import_scheduled) }
-
- subject { described_class.new }
+ describe 'modules' do
+ it 'includes ProjectImportOptions' do
+ expect(described_class).to include_module(ProjectImportOptions)
+ end
+ end
describe '#perform' do
+ let(:project) { create(:project, :import_scheduled) }
+
context 'when worker was reset without cleanup' do
let(:jid) { '12345678' }
let(:started_project) { create(:project, :import_started, import_jid: jid) }
@@ -44,22 +48,11 @@ describe RepositoryImportWorker do
expect do
subject.perform(project.id)
- end.to raise_error(RepositoryImportWorker::ImportError, error)
+ end.to raise_error(StandardError, error)
expect(project.reload.import_jid).not_to be_nil
end
end
- context 'with unexpected error' do
- it 'marks import as failed' do
- allow_any_instance_of(Projects::ImportService).to receive(:execute).and_raise(RuntimeError)
-
- expect do
- subject.perform(project.id)
- end.to raise_error(RepositoryImportWorker::ImportError)
- expect(project.reload.import_status).to eq('failed')
- end
- end
-
context 'when using an asynchronous importer' do
it 'does not mark the import process as finished' do
service = double(:service)
diff --git a/yarn.lock b/yarn.lock
index c4d1bd3c682..26e2b790f98 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -1704,14 +1704,112 @@ custom-event@~1.0.0:
version "1.0.1"
resolved "https://registry.yarnpkg.com/custom-event/-/custom-event-1.0.1.tgz#5d02a46850adf1b4a317946a3928fccb5bfd0425"
+d3-array@^1.2.0, d3-array@^1.2.1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-array/-/d3-array-1.2.1.tgz#d1ca33de2f6ac31efadb8e050a021d7e2396d5dc"
+
+d3-axis@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-axis/-/d3-axis-1.0.8.tgz#31a705a0b535e65759de14173a31933137f18efa"
+
+d3-brush@^1.0.4:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-brush/-/d3-brush-1.0.4.tgz#00c2f238019f24f6c0a194a26d41a1530ffe7bc4"
+ dependencies:
+ d3-dispatch "1"
+ d3-drag "1"
+ d3-interpolate "1"
+ d3-selection "1"
+ d3-transition "1"
+
+d3-collection@1:
+ version "1.0.4"
+ resolved "https://registry.yarnpkg.com/d3-collection/-/d3-collection-1.0.4.tgz#342dfd12837c90974f33f1cc0a785aea570dcdc2"
+
+d3-color@1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-color/-/d3-color-1.0.3.tgz#bc7643fca8e53a8347e2fbdaffa236796b58509b"
+
+d3-dispatch@1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-dispatch/-/d3-dispatch-1.0.3.tgz#46e1491eaa9b58c358fce5be4e8bed626e7871f8"
+
+d3-drag@1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-drag/-/d3-drag-1.2.1.tgz#df8dd4c502fb490fc7462046a8ad98a5c479282d"
+ dependencies:
+ d3-dispatch "1"
+ d3-selection "1"
+
+d3-ease@1:
+ version "1.0.3"
+ resolved "https://registry.yarnpkg.com/d3-ease/-/d3-ease-1.0.3.tgz#68bfbc349338a380c44d8acc4fbc3304aa2d8c0e"
+
+d3-format@1:
+ version "1.2.1"
+ resolved "https://registry.yarnpkg.com/d3-format/-/d3-format-1.2.1.tgz#4e19ecdb081a341dafaf5f555ee956bcfdbf167f"
+
+d3-interpolate@1:
+ version "1.1.6"
+ resolved "https://registry.yarnpkg.com/d3-interpolate/-/d3-interpolate-1.1.6.tgz#2cf395ae2381804df08aa1bf766b7f97b5f68fb6"
+ dependencies:
+ d3-color "1"
+
+d3-path@1:
+ version "1.0.5"
+ resolved "https://registry.yarnpkg.com/d3-path/-/d3-path-1.0.5.tgz#241eb1849bd9e9e8021c0d0a799f8a0e8e441764"
+
+d3-scale@^1.0.7:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-scale/-/d3-scale-1.0.7.tgz#fa90324b3ea8a776422bd0472afab0b252a0945d"
+ dependencies:
+ d3-array "^1.2.0"
+ d3-collection "1"
+ d3-color "1"
+ d3-format "1"
+ d3-interpolate "1"
+ d3-time "1"
+ d3-time-format "2"
+
+d3-selection@1, d3-selection@^1.1.0, d3-selection@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-selection/-/d3-selection-1.2.0.tgz#1b8ec1c7cedadfb691f2ba20a4a3cfbeb71bbc88"
+
+d3-shape@^1.2.0:
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/d3-shape/-/d3-shape-1.2.0.tgz#45d01538f064bafd05ea3d6d2cb748fd8c41f777"
+ dependencies:
+ d3-path "1"
+
+d3-time-format@2, d3-time-format@^2.1.1:
+ version "2.1.1"
+ resolved "https://registry.yarnpkg.com/d3-time-format/-/d3-time-format-2.1.1.tgz#85b7cdfbc9ffca187f14d3c456ffda268081bb31"
+ dependencies:
+ d3-time "1"
+
+d3-time@1, d3-time@^1.0.8:
+ version "1.0.8"
+ resolved "https://registry.yarnpkg.com/d3-time/-/d3-time-1.0.8.tgz#dbd2d6007bf416fe67a76d17947b784bffea1e84"
+
+d3-timer@1:
+ version "1.0.7"
+ resolved "https://registry.yarnpkg.com/d3-timer/-/d3-timer-1.0.7.tgz#df9650ca587f6c96607ff4e60cc38229e8dd8531"
+
+d3-transition@1:
+ version "1.1.1"
+ resolved "https://registry.yarnpkg.com/d3-transition/-/d3-transition-1.1.1.tgz#d8ef89c3b848735b060e54a39b32aaebaa421039"
+ dependencies:
+ d3-color "1"
+ d3-dispatch "1"
+ d3-ease "1"
+ d3-interpolate "1"
+ d3-selection "^1.1.0"
+ d3-timer "1"
+
d3@3.5.17:
version "3.5.17"
resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.17.tgz#bc46748004378b21a360c9fc7cf5231790762fb8"
-d3@^3.5.11:
- version "3.5.11"
- resolved "https://registry.yarnpkg.com/d3/-/d3-3.5.11.tgz#d130750eed0554db70e8432102f920a12407b69c"
-
d@1:
version "1.0.0"
resolved "https://registry.yarnpkg.com/d/-/d-1.0.0.tgz#754bb5bfe55451da69a58b94d45f4c5b0462d58f"
@@ -6430,6 +6528,10 @@ vue-resource@^1.3.4:
dependencies:
got "^7.0.0"
+vue-router@^3.0.1:
+ version "3.0.1"
+ resolved "https://registry.yarnpkg.com/vue-router/-/vue-router-3.0.1.tgz#d9b05ad9c7420ba0f626d6500d693e60092cc1e9"
+
vue-style-loader@^3.0.0:
version "3.0.3"
resolved "https://registry.yarnpkg.com/vue-style-loader/-/vue-style-loader-3.0.3.tgz#623658f81506aef9d121cdc113a4f5c9cac32df7"