summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorPawel Chojnacki <pawel@chojnacki.ws>2018-03-06 16:08:11 +0100
committerPawel Chojnacki <pawel@chojnacki.ws>2018-03-06 16:08:11 +0100
commit4ff8db0d2e8371dfdae2ddef8a8595c1ef80c3d4 (patch)
tree3ff849c51603151baaf85ee0436d9c2f3e47915d
parent60f5250893626620920d2f108df3b3e98d974f0e (diff)
parent5e8138aa54492dd3ace42d889ba01f82e8e19c83 (diff)
downloadgitlab-ce-5029-support-cluster-metrics-ce.tar.gz
Merge remote-tracking branch 'upstream/master' into 5029-support-cluster-metrics-ce5029-support-cluster-metrics-ce
# Conflicts: # spec/models/clusters/applications/prometheus_spec.rb
-rw-r--r--.codeclimate.yml5
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml5
-rw-r--r--.gitlab/issue_templates/Bug.md6
-rw-r--r--.rubocop.yml1
-rw-r--r--.rubocop_todo.yml6
-rw-r--r--CHANGELOG.md21
-rw-r--r--CONTRIBUTING.md17
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile6
-rw-r--r--Gemfile.lock7
-rw-r--r--app/assets/javascripts/autosave.js32
-rw-r--r--app/assets/javascripts/awards_handler.js16
-rw-r--r--app/assets/javascripts/behaviors/toggler_behavior.js4
-rw-r--r--app/assets/javascripts/blob/balsamiq_viewer.js4
-rw-r--r--app/assets/javascripts/blob/notebook_viewer.js2
-rw-r--r--app/assets/javascripts/blob/pdf_viewer.js2
-rw-r--r--app/assets/javascripts/blob/sketch_viewer.js4
-rw-r--r--app/assets/javascripts/blob/stl_viewer.js4
-rw-r--r--app/assets/javascripts/blob/viewer/index.js33
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.vue (renamed from app/assets/javascripts/boards/components/board_new_issue.js)91
-rw-r--r--app/assets/javascripts/boards/components/board_sidebar.js2
-rw-r--r--app/assets/javascripts/boards/filtered_search_boards.js4
-rw-r--r--app/assets/javascripts/boards/index.js (renamed from app/assets/javascripts/boards/boards_bundle.js)16
-rw-r--r--app/assets/javascripts/boards/models/issue.js2
-rw-r--r--app/assets/javascripts/boards/stores/boards_store.js2
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js4
-rw-r--r--app/assets/javascripts/clusters/components/application_row.vue6
-rw-r--r--app/assets/javascripts/clusters/components/applications.vue157
-rw-r--r--app/assets/javascripts/clusters/constants.js1
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js10
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js4
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue38
-rw-r--r--app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js6
-rw-r--r--app/assets/javascripts/deploy_keys/index.js4
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js1
-rw-r--r--app/assets/javascripts/diff_notes/diff_notes_bundle.js15
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js5
-rw-r--r--app/assets/javascripts/dispatcher.js237
-rw-r--r--app/assets/javascripts/environments/components/environments_table.vue2
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_bundle.js4
-rw-r--r--app/assets/javascripts/environments/index.js (renamed from app/assets/javascripts/environments/environments_bundle.js)4
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js102
-rw-r--r--app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue104
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_bundle.js10
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js29
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_manager.js22
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js23
-rw-r--r--app/assets/javascripts/filtered_search/recent_searches_root.js4
-rw-r--r--app/assets/javascripts/groups/components/app.vue10
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list.vue65
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue35
-rw-r--r--app/assets/javascripts/ide/components/commit_sidebar/list_item.vue36
-rw-r--r--app/assets/javascripts/ide/components/ide.vue99
-rw-r--r--app/assets/javascripts/ide/components/ide_context_bar.vue108
-rw-r--r--app/assets/javascripts/ide/components/ide_project_branches_tree.vue47
-rw-r--r--app/assets/javascripts/ide/components/ide_project_tree.vue49
-rw-r--r--app/assets/javascripts/ide/components/ide_repo_tree.vue74
-rw-r--r--app/assets/javascripts/ide/components/ide_side_bar.vue114
-rw-r--r--app/assets/javascripts/ide/components/ide_status_bar.vue66
-rw-r--r--app/assets/javascripts/ide/components/new_branch_form.vue108
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/index.vue101
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/modal.vue112
-rw-r--r--app/assets/javascripts/ide/components/new_dropdown/upload.vue87
-rw-r--r--app/assets/javascripts/ide/components/repo_commit_section.vue171
-rw-r--r--app/assets/javascripts/ide/components/repo_edit_button.vue57
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue136
-rw-r--r--app/assets/javascripts/ide/components/repo_file.vue165
-rw-r--r--app/assets/javascripts/ide/components/repo_file_buttons.vue60
-rw-r--r--app/assets/javascripts/ide/components/repo_loading_file.vue42
-rw-r--r--app/assets/javascripts/ide/components/repo_prev_directory.vue32
-rw-r--r--app/assets/javascripts/ide/components/repo_preview.vue71
-rw-r--r--app/assets/javascripts/ide/components/repo_tab.vue74
-rw-r--r--app/assets/javascripts/ide/components/repo_tabs.vue27
-rw-r--r--app/assets/javascripts/ide/ide_router.js101
-rw-r--r--app/assets/javascripts/ide/index.js31
-rw-r--r--app/assets/javascripts/ide/lib/common/disposable.js14
-rw-r--r--app/assets/javascripts/ide/lib/common/model.js64
-rw-r--r--app/assets/javascripts/ide/lib/common/model_manager.js32
-rw-r--r--app/assets/javascripts/ide/lib/decorations/controller.js43
-rw-r--r--app/assets/javascripts/ide/lib/diff/controller.js71
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff.js30
-rw-r--r--app/assets/javascripts/ide/lib/diff/diff_worker.js10
-rw-r--r--app/assets/javascripts/ide/lib/editor.js110
-rw-r--r--app/assets/javascripts/ide/lib/editor_options.js2
-rw-r--r--app/assets/javascripts/ide/monaco_loader.js16
-rw-r--r--app/assets/javascripts/ide/services/index.js47
-rw-r--r--app/assets/javascripts/ide/stores/actions.js196
-rw-r--r--app/assets/javascripts/ide/stores/actions/branch.js43
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js137
-rw-r--r--app/assets/javascripts/ide/stores/actions/project.js27
-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.js15
-rw-r--r--app/assets/javascripts/ide/stores/mutation_types.js46
-rw-r--r--app/assets/javascripts/ide/stores/mutations.js70
-rw-r--r--app/assets/javascripts/ide/stores/mutations/branch.js28
-rw-r--r--app/assets/javascripts/ide/stores/mutations/file.js74
-rw-r--r--app/assets/javascripts/ide/stores/mutations/project.js23
-rw-r--r--app/assets/javascripts/ide/stores/mutations/tree.js36
-rw-r--r--app/assets/javascripts/ide/stores/state.js23
-rw-r--r--app/assets/javascripts/ide/stores/utils.js177
-rw-r--r--app/assets/javascripts/labels_select.js48
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js29
-rw-r--r--app/assets/javascripts/lib/utils/text_utility.js14
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js4
-rw-r--r--app/assets/javascripts/merge_request_tabs.js4
-rw-r--r--app/assets/javascripts/milestone_select.js3
-rw-r--r--app/assets/javascripts/mr_notes/index.js41
-rw-r--r--app/assets/javascripts/network/network_bundle.js17
-rw-r--r--app/assets/javascripts/notes.js138
-rw-r--r--app/assets/javascripts/notes/components/comment_form.vue77
-rw-r--r--app/assets/javascripts/notes/components/diff_file_header.vue92
-rw-r--r--app/assets/javascripts/notes/components/diff_with_note.vue96
-rw-r--r--app/assets/javascripts/notes/components/discussion_counter.vue119
-rw-r--r--app/assets/javascripts/notes/components/note_actions.vue61
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue4
-rw-r--r--app/assets/javascripts/notes/components/note_form.vue43
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue13
-rw-r--r--app/assets/javascripts/notes/components/noteable_discussion.vue198
-rw-r--r--app/assets/javascripts/notes/components/noteable_note.vue17
-rw-r--r--app/assets/javascripts/notes/components/notes_app.vue43
-rw-r--r--app/assets/javascripts/notes/constants.js6
-rw-r--r--app/assets/javascripts/notes/index.js12
-rw-r--r--app/assets/javascripts/notes/mixins/autosave.js5
-rw-r--r--app/assets/javascripts/notes/mixins/noteable.js22
-rw-r--r--app/assets/javascripts/notes/mixins/resolvable.js50
-rw-r--r--app/assets/javascripts/notes/services/notes_service.js7
-rw-r--r--app/assets/javascripts/notes/stores/actions.js17
-rw-r--r--app/assets/javascripts/notes/stores/getters.js36
-rw-r--r--app/assets/javascripts/notes/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/notes/stores/mutations.js43
-rw-r--r--app/assets/javascripts/notes/stores/utils.js1
-rw-r--r--app/assets/javascripts/pages/admin/abuse_reports/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js4
-rw-r--r--app/assets/javascripts/pages/admin/broadcast_messages/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/cohorts/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/groups/show/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/edit/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/labels/new/index.js2
-rw-r--r--app/assets/javascripts/pages/admin/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/dashboard/todos/index/todos.js6
-rw-r--r--app/assets/javascripts/pages/profiles/index.js16
-rw-r--r--app/assets/javascripts/pages/profiles/index/index.js4
-rw-r--r--app/assets/javascripts/pages/profiles/two_factor_auths/index.js (renamed from app/assets/javascripts/two_factor_auth.js)2
-rw-r--r--app/assets/javascripts/pages/projects/boards/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/pipelines/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/commit/show/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/compare/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/cycle_analytics/show/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/folder/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/environments/terminal/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/issues/show.js13
-rw-r--r--app/assets/javascripts/pages/projects/issues/show/index.js13
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js7
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js32
-rw-r--r--app/assets/javascripts/pages/projects/merge_requests/show/index.js33
-rw-r--r--app/assets/javascripts/pages/projects/network/network.js (renamed from app/assets/javascripts/network/network.js)2
-rw-r--r--app/assets/javascripts/pages/projects/network/show/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/new/index.js4
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/builds/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/index/index.js40
-rw-r--r--app/assets/javascripts/pages/projects/pipelines/show/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/registry/repositories/index.js3
-rw-r--r--app/assets/javascripts/pages/projects/settings/repository/show/index.js16
-rw-r--r--app/assets/javascripts/pages/projects/snippets/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/snippets/new/index.js6
-rw-r--r--app/assets/javascripts/pages/projects/wikis/index.js4
-rw-r--r--app/assets/javascripts/pages/search/init_filtered_search.js18
-rw-r--r--app/assets/javascripts/pages/snippets/edit/index.js6
-rw-r--r--app/assets/javascripts/pages/snippets/new/index.js6
-rw-r--r--app/assets/javascripts/pipelines/components/blank_state.vue32
-rw-r--r--app/assets/javascripts/pipelines/components/empty_state.vue49
-rw-r--r--app/assets/javascripts/pipelines/components/error_state.vue26
-rw-r--r--app/assets/javascripts/pipelines/components/nav_controls.vue81
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue250
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue2
-rw-r--r--app/assets/javascripts/pipelines/mixins/pipelines.js25
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js4
-rw-r--r--app/assets/javascripts/pipelines/pipelines_bundle.js27
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js1
-rw-r--r--app/assets/javascripts/profile/profile.js150
-rw-r--r--app/assets/javascripts/profile/profile_bundle.js2
-rw-r--r--app/assets/javascripts/protected_branches/index.js9
-rw-r--r--app/assets/javascripts/protected_tags/index.js9
-rw-r--r--app/assets/javascripts/registry/index.js4
-rw-r--r--app/assets/javascripts/shortcuts_issuable.js7
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.js224
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/assignees.vue232
-rw-r--r--app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js6
-rw-r--r--app/assets/javascripts/sidebar/sidebar_bundle.js8
-rw-r--r--app/assets/javascripts/snippet/snippet_bundle.js13
-rw-r--r--app/assets/javascripts/terminal/index.js (renamed from app/assets/javascripts/terminal/terminal_bundle.js)2
-rw-r--r--app/assets/javascripts/test.js1
-rw-r--r--app/assets/javascripts/u2f/authenticate.js26
-rw-r--r--app/assets/javascripts/u2f/register.js27
-rw-r--r--app/assets/javascripts/u2f/util.js42
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue2
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/index.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/clipboard_button.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/expand_button.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/issue/issue_warning.vue3
-rw-r--r--app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue24
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue10
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue149
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue78
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue84
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue34
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue21
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue22
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue27
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue30
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue63
-rw-r--r--app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue48
-rw-r--r--app/assets/javascripts/vue_shared/models/label.js (renamed from app/assets/javascripts/boards/models/label.js)4
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/assets/stylesheets/pages/issuable.scss7
-rw-r--r--app/assets/stylesheets/pages/issues.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/impersonation_tokens_controller.rb1
-rw-r--r--app/controllers/application_controller.rb12
-rw-r--r--app/controllers/concerns/issuable_actions.rb14
-rw-r--r--app/controllers/concerns/issuable_collections.rb2
-rw-r--r--app/controllers/concerns/membership_actions.rb64
-rw-r--r--app/controllers/concerns/notes_actions.rb14
-rw-r--r--app/controllers/groups/application_controller.rb4
-rw-r--r--app/controllers/groups/group_members_controller.rb29
-rw-r--r--app/controllers/groups/labels_controller.rb9
-rw-r--r--app/controllers/groups_controller.rb1
-rw-r--r--app/controllers/ide_controller.rb6
-rw-r--r--app/controllers/import/bitbucket_controller.rb2
-rw-r--r--app/controllers/invites_controller.rb2
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb22
-rw-r--r--app/controllers/profiles/passwords_controller.rb1
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/branches_controller.rb41
-rw-r--r--app/controllers/projects/clusters_controller.rb5
-rw-r--r--app/controllers/projects/commits_controller.rb44
-rw-r--r--app/controllers/projects/compare_controller.rb6
-rw-r--r--app/controllers/projects/discussions_controller.rb38
-rw-r--r--app/controllers/projects/issues_controller.rb14
-rw-r--r--app/controllers/projects/network_controller.rb25
-rw-r--r--app/controllers/projects/notes_controller.rb30
-rw-r--r--app/controllers/projects/pages_domains_controller.rb29
-rw-r--r--app/controllers/projects/project_members_controller.rb29
-rw-r--r--app/controllers/projects/tree_controller.rb2
-rw-r--r--app/controllers/projects_controller.rb16
-rw-r--r--app/controllers/sessions_controller.rb2
-rw-r--r--app/finders/branches_finder.rb2
-rw-r--r--app/finders/issuable_finder.rb12
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/labels_finder.rb25
-rw-r--r--app/finders/merge_requests_finder.rb32
-rw-r--r--app/finders/notes_finder.rb12
-rw-r--r--app/finders/snippets_finder.rb28
-rw-r--r--app/finders/todos_finder.rb13
-rw-r--r--app/helpers/application_helper.rb4
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/helpers/auth_helper.rb6
-rw-r--r--app/helpers/blob_helper.rb137
-rw-r--r--app/helpers/branches_helper.rb11
-rw-r--r--app/helpers/groups_helper.rb18
-rw-r--r--app/helpers/import_helper.rb40
-rw-r--r--app/helpers/issuables_helper.rb4
-rw-r--r--app/helpers/labels_helper.rb1
-rw-r--r--app/helpers/notes_helper.rb33
-rw-r--r--app/helpers/profiles_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb8
-rw-r--r--app/helpers/search_helper.rb2
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/helpers/tree_helper.rb4
-rw-r--r--app/helpers/u2f_helper.rb5
-rw-r--r--app/models/badge.rb51
-rw-r--r--app/models/badges/group_badge.rb5
-rw-r--r--app/models/badges/project_badge.rb15
-rw-r--r--app/models/chat_name.rb21
-rw-r--r--app/models/ci/group_variable.rb5
-rw-r--r--app/models/ci/runner.rb4
-rw-r--r--app/models/ci/variable.rb5
-rw-r--r--app/models/clusters/applications/helm.rb2
-rw-r--r--app/models/clusters/applications/ingress.rb28
-rw-r--r--app/models/clusters/applications/prometheus.rb11
-rw-r--r--app/models/clusters/applications/runner.rb69
-rw-r--r--app/models/clusters/cluster.rb7
-rw-r--r--app/models/clusters/concerns/application_core.rb5
-rw-r--r--app/models/clusters/concerns/application_data.rb23
-rw-r--r--app/models/commit.rb6
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/access_requestable.rb2
-rw-r--r--app/models/concerns/issuable.rb1
-rw-r--r--app/models/concerns/updated_at_filterable.rb12
-rw-r--r--app/models/cycle_analytics.rb6
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/identity.rb6
-rw-r--r--app/models/lfs_object.rb4
-rw-r--r--app/models/member.rb10
-rw-r--r--app/models/merge_request_diff.rb4
-rw-r--r--app/models/network/commit.rb7
-rw-r--r--app/models/note.rb1
-rw-r--r--app/models/project.rb59
-rw-r--r--app/models/project_services/asana_service.rb2
-rw-r--r--app/models/project_services/campfire_service.rb2
-rw-r--r--app/models/project_services/chat_notification_service.rb14
-rw-r--r--app/models/project_services/hipchat_service.rb4
-rw-r--r--app/models/project_services/jira_service.rb6
-rw-r--r--app/models/project_services/mattermost_slash_commands_service.rb2
-rw-r--r--app/models/project_services/pushover_service.rb4
-rw-r--r--app/models/project_services/slash_commands_service.rb6
-rw-r--r--app/models/repository.rb17
-rw-r--r--app/models/todo.rb14
-rw-r--r--app/models/tree.rb20
-rw-r--r--app/models/user.rb13
-rw-r--r--app/models/user_synced_attributes_metadata.rb2
-rw-r--r--app/serializers/analytics_stage_entity.rb3
-rw-r--r--app/serializers/cluster_application_entity.rb1
-rw-r--r--app/serializers/diff_file_entity.rb41
-rw-r--r--app/serializers/discussion_entity.rb38
-rw-r--r--app/serializers/merge_request_widget_entity.rb12
-rw-r--r--app/serializers/note_entity.rb12
-rw-r--r--app/services/badges/base_service.rb11
-rw-r--r--app/services/badges/build_service.rb12
-rw-r--r--app/services/badges/create_service.rb10
-rw-r--r--app/services/badges/update_service.rb12
-rw-r--r--app/services/chat_names/find_user_service.rb4
-rw-r--r--app/services/ci/create_trace_artifact_service.rb30
-rw-r--r--app/services/clusters/applications/check_ingress_ip_address_service.rb36
-rw-r--r--app/services/issuable_base_service.rb20
-rw-r--r--app/services/labels/find_or_create_service.rb22
-rw-r--r--app/services/members/approve_access_request_service.rb45
-rw-r--r--app/services/members/authorized_destroy_service.rb61
-rw-r--r--app/services/members/base_service.rb49
-rw-r--r--app/services/members/create_service.rb15
-rw-r--r--app/services/members/destroy_service.rb77
-rw-r--r--app/services/members/request_access_service.rb13
-rw-r--r--app/services/members/update_service.rb16
-rw-r--r--app/services/merge_requests/base_service.rb11
-rw-r--r--app/services/merge_requests/create_service.rb6
-rw-r--r--app/services/merge_requests/update_service.rb11
-rw-r--r--app/services/notes/quick_actions_service.rb8
-rw-r--r--app/services/projects/update_pages_service.rb35
-rw-r--r--app/services/projects/update_service.rb15
-rw-r--r--app/services/quick_actions/interpret_service.rb6
-rw-r--r--app/services/system_hooks_service.rb6
-rw-r--r--app/validators/url_placeholder_validator.rb32
-rw-r--r--app/validators/variable_duplicates_validator.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml16
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/admin/groups/show.html.haml4
-rw-r--r--app/views/admin/identities/_form.html.haml2
-rw-r--r--app/views/admin/identities/_identity.html.haml2
-rw-r--r--app/views/admin/projects/show.html.haml6
-rw-r--r--app/views/admin/runners/_runner.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml1
-rw-r--r--app/views/admin/runners/show.html.haml6
-rw-r--r--app/views/admin/users/projects.html.haml4
-rw-r--r--app/views/devise/sessions/two_factor.html.haml4
-rw-r--r--app/views/groups/group_members/update.js.haml4
-rw-r--r--app/views/groups/issues.html.haml7
-rw-r--r--app/views/groups/merge_requests.html.haml5
-rw-r--r--app/views/groups/projects.html.haml2
-rw-r--r--app/views/ide/index.html.haml11
-rw-r--r--app/views/invites/show.html.haml2
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/nav/projects_dropdown/_show.html.haml2
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml5
-rw-r--r--app/views/layouts/project.html.haml2
-rw-r--r--app/views/notify/project_was_exported_email.html.haml2
-rw-r--r--app/views/notify/project_was_moved_email.html.haml2
-rw-r--r--app/views/profiles/_head.html.haml2
-rw-r--r--app/views/profiles/accounts/show.html.haml1
-rw-r--r--app/views/profiles/audit_log.html.haml1
-rw-r--r--app/views/profiles/chat_names/_chat_name.html.haml2
-rw-r--r--app/views/profiles/chat_names/index.html.haml1
-rw-r--r--app/views/profiles/emails/index.html.haml1
-rw-r--r--app/views/profiles/gpg_keys/index.html.haml1
-rw-r--r--app/views/profiles/keys/index.html.haml5
-rw-r--r--app/views/profiles/keys/show.html.haml1
-rw-r--r--app/views/profiles/notifications/show.html.haml1
-rw-r--r--app/views/profiles/personal_access_tokens/index.html.haml1
-rw-r--r--app/views/profiles/preferences/show.html.haml1
-rw-r--r--app/views/profiles/show.html.haml1
-rw-r--r--app/views/profiles/two_factor_auths/show.html.haml7
-rw-r--r--app/views/projects/_home_panel.html.haml6
-rw-r--r--app/views/projects/_issuable_by_email.html.haml9
-rw-r--r--app/views/projects/_merge_request_fast_forward_settings.html.haml2
-rw-r--r--app/views/projects/_new_project_fields.html.haml2
-rw-r--r--app/views/projects/blob/_header.html.haml3
-rw-r--r--app/views/projects/blob/_upload.html.haml3
-rw-r--r--app/views/projects/blob/_viewer.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_balsamiq.html.haml3
-rw-r--r--app/views/projects/blob/viewers/_notebook.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_pdf.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_sketch.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_stl.html.haml3
-rw-r--r--app/views/projects/branches/_panel.html.haml19
-rw-r--r--app/views/projects/branches/index.html.haml53
-rw-r--r--app/views/projects/branches/new.html.haml1
-rw-r--r--app/views/projects/ci/builds/_build.html.haml2
-rw-r--r--app/views/projects/clusters/show.html.haml2
-rw-r--r--app/views/projects/commit/_pipelines_list.haml4
-rw-r--r--app/views/projects/commit/show.html.haml3
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/cycle_analytics/show.html.haml1
-rw-r--r--app/views/projects/diffs/_file.html.haml2
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/views/projects/environments/folder.html.haml4
-rw-r--r--app/views/projects/environments/index.html.haml1
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/environments/terminal.html.haml1
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/graphs/charts.html.haml1
-rw-r--r--app/views/projects/imports/show.html.haml13
-rw-r--r--app/views/projects/issues/_discussion.html.haml14
-rw-r--r--app/views/projects/issues/_merge_requests.html.haml2
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/merge_requests/conflicts.html.haml2
-rw-r--r--app/views/projects/merge_requests/conflicts/show.html.haml2
-rw-r--r--app/views/projects/merge_requests/show.html.haml49
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml31
-rw-r--r--app/views/projects/pages_domains/_form.html.haml56
-rw-r--r--app/views/projects/pages_domains/edit.html.haml11
-rw-r--r--app/views/projects/pages_domains/new.html.haml6
-rw-r--r--app/views/projects/pages_domains/show.html.haml4
-rw-r--r--app/views/projects/pipelines/charts/_pipeline_times.haml1
-rw-r--r--app/views/projects/pipelines/charts/_pipelines.haml1
-rw-r--r--app/views/projects/pipelines/index.html.haml12
-rw-r--r--app/views/projects/pipelines/new.html.haml1
-rw-r--r--app/views/projects/pipelines/show.html.haml1
-rw-r--r--app/views/projects/project_members/update.js.haml4
-rw-r--r--app/views/projects/protected_branches/_index.html.haml3
-rw-r--r--app/views/projects/protected_tags/_index.html.haml3
-rw-r--r--app/views/projects/registry/repositories/index.html.haml1
-rw-r--r--app/views/projects/runners/_form.html.haml5
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml4
-rw-r--r--app/views/projects/services/prometheus/_configuration_banner.html.haml26
-rw-r--r--app/views/projects/services/prometheus/_help.html.haml28
-rw-r--r--app/views/projects/services/slack_slash_commands/_help.html.haml2
-rw-r--r--app/views/projects/settings/repository/show.html.haml1
-rw-r--r--app/views/projects/tags/new.html.haml1
-rw-r--r--app/views/projects/tree/_tree_header.html.haml5
-rw-r--r--app/views/search/_category.html.haml8
-rw-r--r--app/views/search/_filter.html.haml2
-rw-r--r--app/views/search/_results.html.haml2
-rw-r--r--app/views/search/results/_issue.html.haml2
-rw-r--r--app/views/search/results/_merge_request.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/search/results/_snippet_title.html.haml2
-rw-r--r--app/views/sent_notifications/unsubscribe.html.haml2
-rw-r--r--app/views/shared/_import_form.html.haml19
-rw-r--r--app/views/shared/_ref_switcher.html.haml12
-rw-r--r--app/views/shared/boards/_show.html.haml2
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml6
-rw-r--r--app/views/shared/members/update.js.haml6
-rw-r--r--app/views/shared/milestones/_issuable.html.haml2
-rw-r--r--app/views/shared/milestones/_milestone.html.haml2
-rw-r--r--app/views/shared/milestones/_sidebar.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml2
-rw-r--r--app/views/shared/notes/_notes_with_form.html.haml8
-rw-r--r--app/views/shared/snippets/_form.html.haml1
-rw-r--r--app/views/shared/snippets/_snippet.html.haml2
-rw-r--r--app/views/u2f/_authenticate.html.haml1
-rw-r--r--app/views/u2f/_register.html.haml1
-rw-r--r--app/workers/all_queues.yml2
-rw-r--r--app/workers/authorized_projects_worker.rb36
-rw-r--r--app/workers/cluster_wait_for_ingress_ip_address_worker.rb11
-rw-r--r--app/workers/concerns/gitlab/github_import/object_importer.rb2
-rw-r--r--app/workers/concerns/waitable_worker.rb44
-rw-r--r--app/workers/git_garbage_collect_worker.rb4
-rw-r--r--app/workers/gitlab/github_import/stage/finish_import_worker.rb2
-rw-r--r--app/workers/pages_worker.rb2
-rw-r--r--app/workers/plugin_worker.rb15
-rw-r--r--app/workers/process_commit_worker.rb7
-rw-r--r--app/workers/remove_expired_members_worker.rb2
-rw-r--r--changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml4
-rw-r--r--changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml5
-rw-r--r--changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml4
-rw-r--r--changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml5
-rw-r--r--changelogs/unreleased/33570-slack-notify-default-branch.yml5
-rw-r--r--changelogs/unreleased/34130-null-pipes.yml5
-rw-r--r--changelogs/unreleased/38587-pipelines-empty-state.yml5
-rw-r--r--changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml5
-rw-r--r--changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml5
-rw-r--r--changelogs/unreleased/41461-project-members-slow-due-to-sql.yml5
-rw-r--r--changelogs/unreleased/41616-api-issues-between-date.yml5
-rw-r--r--changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml5
-rw-r--r--changelogs/unreleased/41719-mr-title-fix.yml5
-rw-r--r--changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml5
-rw-r--r--changelogs/unreleased/41851-enable-eslint-codeclimate.yml5
-rw-r--r--changelogs/unreleased/41905_merge_request_and_issue_metrics.yml5
-rw-r--r--changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml5
-rw-r--r--changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml5
-rw-r--r--changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml5
-rw-r--r--changelogs/unreleased/42877-snippets-dashboard-slow.yml5
-rw-r--r--changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml5
-rw-r--r--changelogs/unreleased/43261-fix-prometheus-installation.yml5
-rw-r--r--changelogs/unreleased/43275-improve-variables-validation-message.yml5
-rw-r--r--changelogs/unreleased/43315-gpg-popover.yml5
-rw-r--r--changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml5
-rw-r--r--changelogs/unreleased/43373-fix-cache-index-appending.yml5
-rw-r--r--changelogs/unreleased/43489-display-runner-ip.yml5
-rw-r--r--changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml6
-rw-r--r--changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml5
-rw-r--r--changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml5
-rw-r--r--changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml5
-rw-r--r--changelogs/unreleased/43643-fix-mr-label-filtering.yml5
-rw-r--r--changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml5
-rw-r--r--changelogs/unreleased/43829-update-ssh-addtion-text.yml5
-rw-r--r--changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml5
-rw-r--r--changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml5
-rw-r--r--changelogs/unreleased/an-network-controller-fix.yml5
-rw-r--r--changelogs/unreleased/an-workhorse-3-8-0.yml5
-rw-r--r--changelogs/unreleased/assignees-vue-component-missing-data-container.yml5
-rw-r--r--changelogs/unreleased/dz-namespace-id-not-null.yml5
-rw-r--r--changelogs/unreleased/dz-system-hooks-plugins.yml5
-rw-r--r--changelogs/unreleased/ee-4862-verify-file-checksums.yml5
-rw-r--r--changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml5
-rw-r--r--changelogs/unreleased/feature-edit_pages_domain.yml5
-rw-r--r--changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml5
-rw-r--r--changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml5
-rw-r--r--changelogs/unreleased/fix-500-for-invalid-upload-path.yml5
-rw-r--r--changelogs/unreleased/fix-sm-fix_pages_worker.yml5
-rw-r--r--changelogs/unreleased/fj-28141-redirection-loop.yml5
-rw-r--r--changelogs/unreleased/fj-41174-projects-groups-badges-api.yml5
-rw-r--r--changelogs/unreleased/flipper-caching.yml5
-rw-r--r--changelogs/unreleased/grpc-unavailable-restart.yml5
-rw-r--r--changelogs/unreleased/issue-edit-shortcut.yml5
-rw-r--r--changelogs/unreleased/issue_31081.yml5
-rw-r--r--changelogs/unreleased/jej-fix-slow-lfs-object-check.yml5
-rw-r--r--changelogs/unreleased/jprovazn-scoped-limit.yml6
-rw-r--r--changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml5
-rw-r--r--changelogs/unreleased/kp-label-select-vue.yml5
-rw-r--r--changelogs/unreleased/merge-requests-api-filter-by-branch.yml5
-rw-r--r--changelogs/unreleased/minimal-fix-for-artifacts-service.yml5
-rw-r--r--changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml5
-rw-r--r--changelogs/unreleased/oauth_generic_provider.yml4
-rw-r--r--changelogs/unreleased/refactor-move-assignees-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml5
-rw-r--r--changelogs/unreleased/refactor-move-filtered-search-vue-component.yml5
-rw-r--r--changelogs/unreleased/remove-projects-finder-from-todos-finder.yml5
-rw-r--r--changelogs/unreleased/revert-project-visibility-changes.yml5
-rw-r--r--changelogs/unreleased/sh-cleanup-after-git-gc.yml5
-rw-r--r--changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml5
-rw-r--r--changelogs/unreleased/sh-guard-read-only-user-updates.yml5
-rw-r--r--changelogs/unreleased/users-autocomplete.yml5
-rw-r--r--changelogs/unreleased/wip-new-mr-cmd.yml5
-rw-r--r--changelogs/unreleased/zj-branch-contains-git-message.yml5
-rw-r--r--changelogs/unreleased/zj-gitaly-encoding-issue.yml5
-rw-r--r--changelogs/unreleased/zj-version-string-grouping-ci.yml5
-rw-r--r--config.ru1
-rw-r--r--config/application.rb1
-rw-r--r--config/initializers/devise.rb14
-rw-r--r--config/initializers/doorkeeper.rb2
-rw-r--r--config/initializers/forbid_sidekiq_in_transactions.rb17
-rw-r--r--config/initializers/lograge.rb7
-rw-r--r--config/initializers/omniauth.rb4
-rw-r--r--config/initializers/sidekiq.rb2
-rw-r--r--config/prometheus/additional_metrics.yml12
-rw-r--r--config/routes.rb2
-rw-r--r--config/routes/project.rb3
-rw-r--r--config/routes/repository.rb1
-rw-r--r--config/sidekiq_queues.yml1
-rw-r--r--config/webpack.config.js189
-rw-r--r--db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb9
-rw-r--r--db/migrate/20180214093516_create_badges.rb17
-rw-r--r--db/migrate/20180214155405_create_clusters_applications_runners.rb32
-rw-r--r--db/migrate/20180222043024_add_ip_address_to_runner.rb9
-rw-r--r--db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb7
-rw-r--r--db/migrate/20180304204842_clean_commits_count_migration.rb14
-rw-r--r--db/migrate/20180305144721_add_privileged_to_runner.rb18
-rw-r--r--db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb14
-rw-r--r--db/post_migrate/20180212101928_schedule_build_stage_migration.rb29
-rw-r--r--db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb14
-rw-r--r--db/post_migrate/20180301084653_change_project_namespace_id_not_null.rb29
-rw-r--r--db/schema.rb38
-rw-r--r--doc/README.md54
-rw-r--r--doc/administration/gitaly/index.md24
-rw-r--r--doc/administration/incoming_email.md331
-rw-r--r--doc/administration/index.md12
-rw-r--r--doc/administration/logs.md3
-rw-r--r--doc/administration/plugins.md66
-rw-r--r--doc/administration/raketasks/check.md27
-rw-r--r--doc/administration/reply_by_email.md354
-rw-r--r--doc/administration/reply_by_email_postfix_setup.md12
-rw-r--r--doc/api/README.md2
-rw-r--r--doc/api/branches.md1
-rw-r--r--doc/api/commits.md3
-rw-r--r--doc/api/group_badges.md191
-rw-r--r--doc/api/groups.md4
-rw-r--r--doc/api/issues.md14
-rw-r--r--doc/api/merge_requests.md63
-rw-r--r--doc/api/project_badges.md188
-rw-r--r--doc/api/projects.md9
-rw-r--r--doc/api/services.md1
-rw-r--r--doc/ci/pipelines.md3
-rw-r--r--doc/ci/variables/README.md3
-rw-r--r--doc/development/database_debugging.md35
-rw-r--r--doc/development/ee_features.md39
-rw-r--r--doc/development/emails.md62
-rw-r--r--doc/development/fe_guide/performance.md12
-rw-r--r--doc/development/fe_guide/vue.md10
-rw-r--r--doc/development/i18n/externalization.md221
-rw-r--r--doc/development/i18n/proofreader.md3
-rw-r--r--doc/development/i18n/translation.md45
-rw-r--r--doc/development/rake_tasks.md6
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/integration/saml.md3
-rw-r--r--doc/ssh/README.md51
-rw-r--r--doc/user/markdown.md8
-rw-r--r--doc/user/project/clusters/index.md1
-rw-r--r--doc/user/project/import/img/import_projects_from_repo_url.pngbin0 -> 150259 bytes
-rw-r--r--doc/user/project/import/index.md1
-rw-r--r--doc/user/project/import/perforce.md6
-rw-r--r--doc/user/project/import/repo_by_url.md12
-rw-r--r--doc/user/project/integrations/prometheus_library/kubernetes.md15
-rw-r--r--doc/user/project/issues/create_new_issue.md22
-rw-r--r--doc/user/project/issues/img/new_issue_from_email.pngbin0 -> 13461 bytes
-rw-r--r--doc/user/project/merge_requests/index.md6
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--lib/api/access_requests.rb8
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/badges.rb134
-rw-r--r--lib/api/branches.rb16
-rw-r--r--lib/api/commits.rb11
-rw-r--r--lib/api/entities.rb64
-rw-r--r--lib/api/helpers/badges_helpers.rb28
-rw-r--r--lib/api/helpers/runner.rb18
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/members.rb14
-rw-r--r--lib/api/merge_requests.rb6
-rw-r--r--lib/api/project_hooks.rb1
-rw-r--r--lib/api/runner.rb7
-rw-r--r--lib/api/services.rb71
-rw-r--r--lib/api/v3/entities.rb10
-rw-r--r--lib/api/v3/members.rb2
-rw-r--r--lib/api/v3/project_hooks.rb1
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb15
-rw-r--r--lib/banzai/filter/autolink_filter.rb86
-rw-r--r--lib/banzai/redactor.rb25
-rw-r--r--lib/bitbucket/connection.rb2
-rw-r--r--lib/gitlab/auth.rb32
-rw-r--r--lib/gitlab/auth/database/authentication.rb16
-rw-r--r--lib/gitlab/auth/ldap/access.rb89
-rw-r--r--lib/gitlab/auth/ldap/adapter.rb110
-rw-r--r--lib/gitlab/auth/ldap/auth_hash.rb48
-rw-r--r--lib/gitlab/auth/ldap/authentication.rb68
-rw-r--r--lib/gitlab/auth/ldap/config.rb237
-rw-r--r--lib/gitlab/auth/ldap/dn.rb303
-rw-r--r--lib/gitlab/auth/ldap/person.rb122
-rw-r--r--lib/gitlab/auth/ldap/user.rb54
-rw-r--r--lib/gitlab/auth/o_auth/auth_hash.rb92
-rw-r--r--lib/gitlab/auth/o_auth/authentication.rb21
-rw-r--r--lib/gitlab/auth/o_auth/provider.rb73
-rw-r--r--lib/gitlab/auth/o_auth/session.rb21
-rw-r--r--lib/gitlab/auth/o_auth/user.rb246
-rw-r--r--lib/gitlab/auth/saml/auth_hash.rb19
-rw-r--r--lib/gitlab/auth/saml/config.rb21
-rw-r--r--lib/gitlab/auth/saml/user.rb52
-rw-r--r--lib/gitlab/background_migration/migrate_build_stage.rb48
-rw-r--r--lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb496
-rw-r--r--lib/gitlab/ci/pipeline/chain/create.rb16
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/base.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/equals.rb26
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/null.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/operator.rb15
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/string.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/value.rb15
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexeme/variable.rb25
-rw-r--r--lib/gitlab/ci/pipeline/expression/lexer.rb59
-rw-r--r--lib/gitlab/ci/pipeline/expression/parser.rb40
-rw-r--r--lib/gitlab/ci/pipeline/expression/statement.rb42
-rw-r--r--lib/gitlab/ci/pipeline/expression/token.rb28
-rw-r--r--lib/gitlab/contributions_calendar.rb2
-rw-r--r--lib/gitlab/cycle_analytics/base_query.rb7
-rw-r--r--lib/gitlab/cycle_analytics/base_stage.rb29
-rw-r--r--lib/gitlab/cycle_analytics/production_helper.rb4
-rw-r--r--lib/gitlab/cycle_analytics/test_stage.rb6
-rw-r--r--lib/gitlab/cycle_analytics/usage_data.rb72
-rw-r--r--lib/gitlab/database/median.rb130
-rw-r--r--lib/gitlab/git/blob.rb14
-rw-r--r--lib/gitlab/git/branch.rb14
-rw-r--r--lib/gitlab/git/commit.rb39
-rw-r--r--lib/gitlab/git/lfs_changes.rb26
-rw-r--r--lib/gitlab/git/repository.rb55
-rw-r--r--lib/gitlab/git/tree.rb23
-rw-r--r--lib/gitlab/git/wiki.rb5
-rw-r--r--lib/gitlab/git_access.rb2
-rw-r--r--lib/gitlab/gitaly_client.rb23
-rw-r--r--lib/gitlab/gitaly_client/blob_service.rb55
-rw-r--r--lib/gitlab/gitaly_client/commit_service.rb26
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb10
-rw-r--r--lib/gitlab/gon_helper.rb2
-rw-r--r--lib/gitlab/gpg/commit.rb20
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml5
-rw-r--r--lib/gitlab/import_export/relation_factory.rb3
-rw-r--r--lib/gitlab/job_waiter.rb8
-rw-r--r--lib/gitlab/kubernetes/config_map.rb37
-rw-r--r--lib/gitlab/kubernetes/helm/api.rb9
-rw-r--r--lib/gitlab/kubernetes/helm/base_command.rb40
-rw-r--r--lib/gitlab/kubernetes/helm/init_command.rb19
-rw-r--r--lib/gitlab/kubernetes/helm/install_command.rb53
-rw-r--r--lib/gitlab/kubernetes/helm/pod.rb50
-rw-r--r--lib/gitlab/ldap/access.rb87
-rw-r--r--lib/gitlab/ldap/adapter.rb108
-rw-r--r--lib/gitlab/ldap/auth_hash.rb46
-rw-r--r--lib/gitlab/ldap/authentication.rb70
-rw-r--r--lib/gitlab/ldap/config.rb235
-rw-r--r--lib/gitlab/ldap/dn.rb301
-rw-r--r--lib/gitlab/ldap/person.rb120
-rw-r--r--lib/gitlab/ldap/user.rb52
-rw-r--r--lib/gitlab/middleware/read_only.rb83
-rw-r--r--lib/gitlab/middleware/read_only/controller.rb86
-rw-r--r--lib/gitlab/middleware/release_env.rb14
-rw-r--r--lib/gitlab/o_auth.rb6
-rw-r--r--lib/gitlab/o_auth/auth_hash.rb90
-rw-r--r--lib/gitlab/o_auth/provider.rb54
-rw-r--r--lib/gitlab/o_auth/session.rb19
-rw-r--r--lib/gitlab/o_auth/user.rb241
-rw-r--r--lib/gitlab/plugin.rb26
-rw-r--r--lib/gitlab/plugin_logger.rb7
-rw-r--r--lib/gitlab/project_search_results.rb29
-rw-r--r--lib/gitlab/saml/auth_hash.rb17
-rw-r--r--lib/gitlab/saml/config.rb19
-rw-r--r--lib/gitlab/saml/user.rb50
-rw-r--r--lib/gitlab/search_results.rb20
-rw-r--r--lib/gitlab/sidekiq_middleware/memory_killer.rb67
-rw-r--r--lib/gitlab/sidekiq_middleware/shutdown.rb133
-rw-r--r--lib/gitlab/slash_commands/base_command.rb7
-rw-r--r--lib/gitlab/slash_commands/command.rb5
-rw-r--r--lib/gitlab/string_placeholder_replacer.rb27
-rw-r--r--lib/gitlab/string_range_marker.rb2
-rw-r--r--lib/gitlab/string_regex_marker.rb12
-rw-r--r--lib/gitlab/usage_data.rb5
-rw-r--r--lib/gitlab/user_access.rb2
-rw-r--r--lib/gitlab/verify/batch_verifier.rb64
-rw-r--r--lib/gitlab/verify/lfs_objects.rb27
-rw-r--r--lib/gitlab/verify/rake_task.rb53
-rw-r--r--lib/gitlab/verify/uploads.rb27
-rw-r--r--lib/haml_lint/inline_javascript.rb6
-rw-r--r--lib/tasks/gitlab/check.rake6
-rw-r--r--lib/tasks/gitlab/cleanup.rake2
-rw-r--r--lib/tasks/gitlab/lfs/check.rake8
-rw-r--r--lib/tasks/gitlab/uploads.rake44
-rw-r--r--lib/tasks/gitlab/uploads/check.rake8
-rw-r--r--lib/tasks/plugins.rake16
-rw-r--r--locale/gitlab.pot222
-rw-r--r--package.json4
-rwxr-xr-xplugins/examples/save_to_file.clj3
-rwxr-xr-xplugins/examples/save_to_file.rb3
-rw-r--r--qa/qa.rb2
-rw-r--r--qa/qa/factory/base.rb2
-rw-r--r--qa/qa/factory/product.rb5
-rw-r--r--qa/qa/factory/repository/push.rb4
-rw-r--r--qa/qa/factory/resource/merge_request.rb9
-rw-r--r--qa/qa/git/repository.rb4
-rw-r--r--qa/qa/page/merge_request/show.rb46
-rw-r--r--qa/qa/page/project/settings/merge_request.rb27
-rw-r--r--qa/qa/specs/features/merge_request/rebase_spec.rb39
-rw-r--r--qa/spec/factory/base_spec.rb17
-rw-r--r--qa/spec/factory/product_spec.rb16
-rwxr-xr-xscripts/security-harness2
-rwxr-xr-xscripts/trigger-build-docs33
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb4
-rw-r--r--spec/controllers/groups/labels_controller_spec.rb33
-rw-r--r--spec/controllers/oauth/authorizations_controller_spec.rb2
-rw-r--r--spec/controllers/projects/branches_controller_spec.rb33
-rw-r--r--spec/controllers/projects/clusters_controller_spec.rb6
-rw-r--r--spec/controllers/projects/cycle_analytics_controller_spec.rb2
-rw-r--r--spec/controllers/projects/discussions_controller_spec.rb26
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pages_domains_controller_spec.rb60
-rw-r--r--spec/factories/badge.rb14
-rw-r--r--spec/factories/clusters/applications/helm.rb1
-rw-r--r--spec/factories/lfs_objects.rb6
-rw-r--r--spec/features/admin/admin_groups_spec.rb2
-rw-r--r--spec/features/admin/admin_projects_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb8
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/admin/services/admin_activates_prometheus_spec.rb21
-rw-r--r--spec/features/cycle_analytics_spec.rb20
-rw-r--r--spec/features/dashboard/issues_spec.rb8
-rw-r--r--spec/features/dashboard/merge_requests_spec.rb4
-rw-r--r--spec/features/dashboard/todos/todos_filtering_spec.rb8
-rw-r--r--spec/features/groups/empty_states_spec.rb124
-rw-r--r--spec/features/groups/members/manage_members_spec.rb (renamed from spec/features/groups/members/manage_members.rb)0
-rw-r--r--spec/features/issues/filtered_search/recent_searches_spec.rb4
-rw-r--r--spec/features/issues/form_spec.rb24
-rw-r--r--spec/features/issues/move_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb3
-rw-r--r--spec/features/merge_request/user_posts_notes_spec.rb2
-rw-r--r--spec/features/profiles/password_spec.rb10
-rw-r--r--spec/features/projects/branches/download_buttons_spec.rb2
-rw-r--r--spec/features/projects/branches_spec.rb137
-rw-r--r--spec/features/projects/clusters/applications_spec.rb38
-rw-r--r--spec/features/projects/environments/environment_spec.rb2
-rw-r--r--spec/features/projects/import_export/test_project_export.tar.gzbin343092 -> 343087 bytes
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb4
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb2
-rw-r--r--spec/features/projects/merge_request_button_spec.rb4
-rw-r--r--spec/features/projects/new_project_spec.rb2
-rw-r--r--spec/features/projects/pages_spec.rb33
-rw-r--r--spec/features/projects/pipelines/pipelines_spec.rb56
-rw-r--r--spec/features/projects/services/user_activates_prometheus_spec.rb23
-rw-r--r--spec/features/projects/settings/user_manages_project_members_spec.rb2
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb57
-rw-r--r--spec/features/projects/tree/create_file_spec.rb47
-rw-r--r--spec/features/projects/tree/upload_file_spec.rb53
-rw-r--r--spec/features/projects_spec.rb8
-rw-r--r--spec/features/search/user_searches_for_code_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_issues_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_merge_requests_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_milestones_spec.rb2
-rw-r--r--spec/features/search/user_searches_for_wiki_pages_spec.rb2
-rw-r--r--spec/features/search/user_uses_search_filters_spec.rb6
-rw-r--r--spec/features/u2f_spec.rb4
-rw-r--r--spec/finders/issues_finder_spec.rb42
-rw-r--r--spec/finders/labels_finder_spec.rb42
-rw-r--r--spec/finders/merge_requests_finder_spec.rb69
-rw-r--r--spec/finders/notes_finder_spec.rb12
-rw-r--r--spec/finders/todos_finder_spec.rb27
-rw-r--r--spec/fixtures/api/schemas/cluster_status.json3
-rw-r--r--spec/fixtures/api/schemas/entities/merge_request_widget.json5
-rw-r--r--spec/fixtures/emails/update_commands_only_reply.eml38
-rw-r--r--spec/helpers/blob_helper_spec.rb8
-rw-r--r--spec/helpers/issuables_helper_spec.rb32
-rw-r--r--spec/helpers/members_helper_spec.rb10
-rw-r--r--spec/helpers/todos_helper_spec.rb4
-rw-r--r--spec/helpers/u2f_helper_spec.rb49
-rw-r--r--spec/javascripts/autosave_spec.js55
-rw-r--r--spec/javascripts/boards/board_card_spec.js2
-rw-r--r--spec/javascripts/boards/board_new_issue_spec.js2
-rw-r--r--spec/javascripts/boards/boards_store_spec.js2
-rw-r--r--spec/javascripts/boards/issue_card_spec.js2
-rw-r--r--spec/javascripts/boards/issue_spec.js2
-rw-r--r--spec/javascripts/boards/list_spec.js2
-rw-r--r--spec/javascripts/boards/modal_store_spec.js2
-rw-r--r--spec/javascripts/ci_variable_list/ci_variable_list_spec.js2
-rw-r--r--spec/javascripts/clusters/clusters_bundle_spec.js2
-rw-r--r--spec/javascripts/clusters/components/application_row_spec.js2
-rw-r--r--spec/javascripts/clusters/components/applications_spec.js71
-rw-r--r--spec/javascripts/clusters/services/mock_data.js1
-rw-r--r--spec/javascripts/clusters/stores/clusters_store_spec.js1
-rw-r--r--spec/javascripts/commit/commit_pipeline_status_component_spec.js2
-rw-r--r--spec/javascripts/cycle_analytics/banner_spec.js2
-rw-r--r--spec/javascripts/cycle_analytics/total_time_component_spec.js2
-rw-r--r--spec/javascripts/environments/emtpy_state_spec.js2
-rw-r--r--spec/javascripts/environments/environment_table_spec.js2
-rw-r--r--spec/javascripts/environments/environments_app_spec.js4
-rw-r--r--spec/javascripts/environments/folder/environments_folder_view_spec.js4
-rw-r--r--spec/javascripts/feature_highlight/feature_highlight_helper_spec.js2
-rw-r--r--spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js3
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js18
-rw-r--r--spec/javascripts/fixtures/merge_requests.rb42
-rw-r--r--spec/javascripts/groups/components/app_spec.js59
-rw-r--r--spec/javascripts/groups/components/group_item_spec.js3
-rw-r--r--spec/javascripts/groups/components/groups_spec.js3
-rw-r--r--spec/javascripts/groups/components/item_actions_spec.js3
-rw-r--r--spec/javascripts/groups/components/item_caret_spec.js2
-rw-r--r--spec/javascripts/groups/components/item_stats_spec.js3
-rw-r--r--spec/javascripts/groups/components/item_stats_value_spec.js2
-rw-r--r--spec/javascripts/groups/components/item_type_icon_spec.js3
-rw-r--r--spec/javascripts/issue_show/components/app_spec.js2
-rw-r--r--spec/javascripts/issue_show/components/description_spec.js2
-rw-r--r--spec/javascripts/jobs/header_spec.js2
-rw-r--r--spec/javascripts/labels_select_spec.js43
-rw-r--r--spec/javascripts/lib/utils/common_utils_spec.js15
-rw-r--r--spec/javascripts/notes/components/comment_form_spec.js23
-rw-r--r--spec/javascripts/notes/components/diff_file_header_spec.js93
-rw-r--r--spec/javascripts/notes/components/diff_with_note_spec.js64
-rw-r--r--spec/javascripts/notes/components/note_app_spec.js5
-rw-r--r--spec/javascripts/notes/components/note_body_spec.js27
-rw-r--r--spec/javascripts/notes/components/note_header_spec.js32
-rw-r--r--spec/javascripts/notes/mock_data.js5
-rw-r--r--spec/javascripts/notes/stores/getters_spec.js4
-rw-r--r--spec/javascripts/notes/stores/mutation_spec.js5
-rw-r--r--spec/javascripts/notes_spec.js11
-rw-r--r--spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js2
-rw-r--r--spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js2
-rw-r--r--spec/javascripts/pipelines/blank_state_spec.js29
-rw-r--r--spec/javascripts/pipelines/empty_state_spec.js28
-rw-r--r--spec/javascripts/pipelines/error_state_spec.js27
-rw-r--r--spec/javascripts/pipelines/graph/job_component_spec.js2
-rw-r--r--spec/javascripts/pipelines/nav_controls_spec.js84
-rw-r--r--spec/javascripts/pipelines/pipelines_spec.js679
-rw-r--r--spec/javascripts/profile/account/components/delete_account_modal_spec.js2
-rw-r--r--spec/javascripts/projects_dropdown/components/app_spec.js2
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js2
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_item_spec.js2
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_search_spec.js2
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js2
-rw-r--r--spec/javascripts/registry/components/app_spec.js2
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js33
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_item_spec.js53
-rw-r--r--spec/javascripts/repo/components/commit_sidebar/list_spec.js59
-rw-r--r--spec/javascripts/repo/components/ide_context_bar_spec.js49
-rw-r--r--spec/javascripts/repo/components/ide_repo_tree_spec.js63
-rw-r--r--spec/javascripts/repo/components/ide_side_bar_spec.js43
-rw-r--r--spec/javascripts/repo/components/ide_spec.js39
-rw-r--r--spec/javascripts/repo/components/new_branch_form_spec.js114
-rw-r--r--spec/javascripts/repo/components/new_dropdown/index_spec.js77
-rw-r--r--spec/javascripts/repo/components/new_dropdown/modal_spec.js237
-rw-r--r--spec/javascripts/repo/components/new_dropdown/upload_spec.js158
-rw-r--r--spec/javascripts/repo/components/repo_commit_section_spec.js140
-rw-r--r--spec/javascripts/repo/components/repo_edit_button_spec.js83
-rw-r--r--spec/javascripts/repo/components/repo_editor_spec.js60
-rw-r--r--spec/javascripts/repo/components/repo_file_buttons_spec.js49
-rw-r--r--spec/javascripts/repo/components/repo_file_spec.js98
-rw-r--r--spec/javascripts/repo/components/repo_loading_file_spec.js63
-rw-r--r--spec/javascripts/repo/components/repo_prev_directory_spec.js45
-rw-r--r--spec/javascripts/repo/components/repo_preview_spec.js37
-rw-r--r--spec/javascripts/repo/components/repo_tab_spec.js108
-rw-r--r--spec/javascripts/repo/components/repo_tabs_spec.js37
-rw-r--r--spec/javascripts/repo/helpers.js16
-rw-r--r--spec/javascripts/repo/lib/common/disposable_spec.js44
-rw-r--r--spec/javascripts/repo/lib/common/model_manager_spec.js81
-rw-r--r--spec/javascripts/repo/lib/common/model_spec.js84
-rw-r--r--spec/javascripts/repo/lib/decorations/controller_spec.js120
-rw-r--r--spec/javascripts/repo/lib/diff/controller_spec.js176
-rw-r--r--spec/javascripts/repo/lib/diff/diff_spec.js80
-rw-r--r--spec/javascripts/repo/lib/editor_options_spec.js7
-rw-r--r--spec/javascripts/repo/lib/editor_spec.js128
-rw-r--r--spec/javascripts/repo/monaco_loader_spec.js13
-rw-r--r--spec/javascripts/repo/stores/actions/branch_spec.js44
-rw-r--r--spec/javascripts/repo/stores/actions/file_spec.js431
-rw-r--r--spec/javascripts/repo/stores/actions/tree_spec.js350
-rw-r--r--spec/javascripts/repo/stores/actions_spec.js432
-rw-r--r--spec/javascripts/repo/stores/getters_spec.js114
-rw-r--r--spec/javascripts/repo/stores/mutations/branch_spec.js18
-rw-r--r--spec/javascripts/repo/stores/mutations/file_spec.js131
-rw-r--r--spec/javascripts/repo/stores/mutations/tree_spec.js71
-rw-r--r--spec/javascripts/repo/stores/mutations_spec.js125
-rw-r--r--spec/javascripts/repo/stores/utils_spec.js119
-rw-r--r--spec/javascripts/sidebar/assignees_spec.js2
-rw-r--r--spec/javascripts/sidebar/lock/edit_form_buttons_spec.js2
-rw-r--r--spec/javascripts/sidebar/participants_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_assignees_spec.js2
-rw-r--r--spec/javascripts/sidebar/sidebar_subscriptions_spec.js2
-rw-r--r--spec/javascripts/sidebar/subscriptions_spec.js2
-rw-r--r--spec/javascripts/test_bundle.js6
-rw-r--r--spec/javascripts/u2f/authenticate_spec.js6
-rw-r--r--spec/javascripts/u2f/register_spec.js4
-rw-r--r--spec/javascripts/u2f/util_spec.js45
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js2
-rw-r--r--spec/javascripts/vue_mr_widget/mr_widget_options_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/ci_badge_link_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/clipboard_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/expand_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/file_icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/gl_modal_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/issue/issue_warning_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/loading_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/modal_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/navigation_tabs_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/panel_resizer_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/pikaday_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js81
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js82
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js84
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js42
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js36
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js37
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js39
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js42
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js74
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js94
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js49
-rw-r--r--spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/toggle_button_spec.js2
-rw-r--r--spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js2
-rw-r--r--spec/lib/backup/repository_spec.rb21
-rw-r--r--spec/lib/banzai/filter/autolink_filter_spec.rb126
-rw-r--r--spec/lib/banzai/filter/label_reference_filter_spec.rb8
-rw-r--r--spec/lib/banzai/redactor_spec.rb10
-rw-r--r--spec/lib/gitlab/auth/ldap/access_spec.rb (renamed from spec/lib/gitlab/ldap/access_spec.rb)16
-rw-r--r--spec/lib/gitlab/auth/ldap/adapter_spec.rb (renamed from spec/lib/gitlab/ldap/adapter_spec.rb)4
-rw-r--r--spec/lib/gitlab/auth/ldap/auth_hash_spec.rb (renamed from spec/lib/gitlab/ldap/auth_hash_spec.rb)4
-rw-r--r--spec/lib/gitlab/auth/ldap/authentication_spec.rb (renamed from spec/lib/gitlab/ldap/authentication_spec.rb)8
-rw-r--r--spec/lib/gitlab/auth/ldap/config_spec.rb (renamed from spec/lib/gitlab/ldap/config_spec.rb)2
-rw-r--r--spec/lib/gitlab/auth/ldap/dn_spec.rb (renamed from spec/lib/gitlab/ldap/dn_spec.rb)50
-rw-r--r--spec/lib/gitlab/auth/ldap/person_spec.rb (renamed from spec/lib/gitlab/ldap/person_spec.rb)4
-rw-r--r--spec/lib/gitlab/auth/ldap/user_spec.rb (renamed from spec/lib/gitlab/ldap/user_spec.rb)4
-rw-r--r--spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb (renamed from spec/lib/gitlab/o_auth/auth_hash_spec.rb)2
-rw-r--r--spec/lib/gitlab/auth/o_auth/provider_spec.rb (renamed from spec/lib/gitlab/o_auth/provider_spec.rb)2
-rw-r--r--spec/lib/gitlab/auth/o_auth/user_spec.rb (renamed from spec/lib/gitlab/o_auth/user_spec.rb)32
-rw-r--r--spec/lib/gitlab/auth/saml/auth_hash_spec.rb (renamed from spec/lib/gitlab/saml/auth_hash_spec.rb)2
-rw-r--r--spec/lib/gitlab/auth/saml/user_spec.rb (renamed from spec/lib/gitlab/saml/user_spec.rb)18
-rw-r--r--spec/lib/gitlab/auth_spec.rb8
-rw-r--r--spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb54
-rw-r--r--spec/lib/gitlab/checks/lfs_integrity_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb39
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb22
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb92
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb44
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb70
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb26
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb85
-rw-r--r--spec/lib/gitlab/ci/pipeline/expression/token_spec.rb45
-rw-r--r--spec/lib/gitlab/contributions_calendar_spec.rb14
-rw-r--r--spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb2
-rw-r--r--spec/lib/gitlab/cycle_analytics/events_spec.rb10
-rw-r--r--spec/lib/gitlab/cycle_analytics/usage_data_spec.rb140
-rw-r--r--spec/lib/gitlab/data_builder/build_spec.rb2
-rw-r--r--spec/lib/gitlab/database/median_spec.rb17
-rw-r--r--spec/lib/gitlab/email/handler/create_note_handler_spec.rb27
-rw-r--r--spec/lib/gitlab/git/blob_spec.rb29
-rw-r--r--spec/lib/gitlab/git/branch_spec.rb64
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb142
-rw-r--r--spec/lib/gitlab/git/lfs_changes_spec.rb38
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb511
-rw-r--r--spec/lib/gitlab/git_access_spec.rb13
-rw-r--r--spec/lib/gitlab/git_access_wiki_spec.rb2
-rw-r--r--spec/lib/gitlab/gitaly_client/blob_service_spec.rb60
-rw-r--r--spec/lib/gitlab/gitaly_client/commit_service_spec.rb4
-rw-r--r--spec/lib/gitlab/gpg/commit_spec.rb12
-rw-r--r--spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb2
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml3
-rw-r--r--spec/lib/gitlab/import_export/project.json767
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb4
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb7
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml9
-rw-r--r--spec/lib/gitlab/kubernetes/config_map_spec.rb25
-rw-r--r--spec/lib/gitlab/kubernetes/helm/api_spec.rb26
-rw-r--r--spec/lib/gitlab/kubernetes/helm/base_command_spec.rb44
-rw-r--r--spec/lib/gitlab/kubernetes/helm/init_command_spec.rb24
-rw-r--r--spec/lib/gitlab/kubernetes/helm/install_command_spec.rb146
-rw-r--r--spec/lib/gitlab/kubernetes/helm/pod_spec.rb20
-rw-r--r--spec/lib/gitlab/middleware/read_only_spec.rb25
-rw-r--r--spec/lib/gitlab/middleware/release_env_spec.rb16
-rw-r--r--spec/lib/gitlab/plugin_spec.rb68
-rw-r--r--spec/lib/gitlab/project_search_results_spec.rb68
-rw-r--r--spec/lib/gitlab/search_results_spec.rb36
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb63
-rw-r--r--spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb88
-rw-r--r--spec/lib/gitlab/slash_commands/command_spec.rb5
-rw-r--r--spec/lib/gitlab/slash_commands/deploy_spec.rb3
-rw-r--r--spec/lib/gitlab/slash_commands/issue_new_spec.rb3
-rw-r--r--spec/lib/gitlab/slash_commands/issue_search_spec.rb3
-rw-r--r--spec/lib/gitlab/slash_commands/issue_show_spec.rb3
-rw-r--r--spec/lib/gitlab/string_placeholder_replacer_spec.rb38
-rw-r--r--spec/lib/gitlab/string_regex_marker_spec.rb35
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb1
-rw-r--r--spec/lib/gitlab/verify/lfs_objects_spec.rb35
-rw-r--r--spec/lib/gitlab/verify/uploads_spec.rb44
-rw-r--r--spec/mailers/notify_spec.rb22
-rw-r--r--spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb2
-rw-r--r--spec/migrations/migrate_issues_to_ghost_user_spec.rb2
-rw-r--r--spec/migrations/migrate_stages_statuses_spec.rb2
-rw-r--r--spec/migrations/schedule_build_stage_migration_spec.rb35
-rw-r--r--spec/models/badge_spec.rb94
-rw-r--r--spec/models/badges/group_badge_spec.rb11
-rw-r--r--spec/models/badges/project_badge_spec.rb43
-rw-r--r--spec/models/chat_name_spec.rb20
-rw-r--r--spec/models/ci/group_variable_spec.rb2
-rw-r--r--spec/models/ci/variable_spec.rb2
-rw-r--r--spec/models/clusters/applications/helm_spec.rb97
-rw-r--r--spec/models/clusters/applications/ingress_spec.rb76
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb43
-rw-r--r--spec/models/clusters/applications/runner_spec.rb99
-rw-r--r--spec/models/clusters/cluster_spec.rb4
-rw-r--r--spec/models/commit_status_spec.rb4
-rw-r--r--spec/models/cycle_analytics/code_spec.rb20
-rw-r--r--spec/models/cycle_analytics/issue_spec.rb8
-rw-r--r--spec/models/cycle_analytics/plan_spec.rb8
-rw-r--r--spec/models/cycle_analytics/production_spec.rb14
-rw-r--r--spec/models/cycle_analytics/review_spec.rb4
-rw-r--r--spec/models/cycle_analytics/staging_spec.rb14
-rw-r--r--spec/models/cycle_analytics/test_spec.rb16
-rw-r--r--spec/models/cycle_analytics_spec.rb30
-rw-r--r--spec/models/group_spec.rb1
-rw-r--r--spec/models/project_services/asana_service_spec.rb2
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb4
-rw-r--r--spec/models/project_services/jira_service_spec.rb3
-rw-r--r--spec/models/project_services/mattermost_slash_commands_service_spec.rb6
-rw-r--r--spec/models/project_spec.rb42
-rw-r--r--spec/models/project_wiki_spec.rb6
-rw-r--r--spec/models/repository_spec.rb52
-rw-r--r--spec/models/user_spec.rb26
-rw-r--r--spec/requests/api/badges_spec.rb367
-rw-r--r--spec/requests/api/branches_spec.rb21
-rw-r--r--spec/requests/api/commits_spec.rb12
-rw-r--r--spec/requests/api/issues_spec.rb40
-rw-r--r--spec/requests/api/merge_requests_spec.rb116
-rw-r--r--spec/requests/api/pages_domains_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb6
-rw-r--r--spec/requests/api/runner_spec.rb24
-rw-r--r--spec/requests/api/v3/issues_spec.rb4
-rw-r--r--spec/requests/api/v3/project_hooks_spec.rb6
-rw-r--r--spec/requests/git_http_spec.rb16
-rw-r--r--spec/requests/openid_connect_spec.rb12
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb6
-rw-r--r--spec/serializers/cluster_application_entity_spec.rb14
-rw-r--r--spec/serializers/diff_file_entity_spec.rb24
-rw-r--r--spec/serializers/discussion_entity_spec.rb36
-rw-r--r--spec/serializers/note_entity_spec.rb11
-rw-r--r--spec/services/auth/container_registry_authentication_service_spec.rb18
-rw-r--r--spec/services/chat_names/find_user_service_spec.rb25
-rw-r--r--spec/services/ci/create_trace_artifact_service_spec.rb46
-rw-r--r--spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb73
-rw-r--r--spec/services/labels/find_or_create_service_spec.rb78
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb70
-rw-r--r--spec/services/members/authorized_destroy_service_spec.rb110
-rw-r--r--spec/services/members/create_service_spec.rb6
-rw-r--r--spec/services/members/destroy_service_spec.rb194
-rw-r--r--spec/services/members/request_access_service_spec.rb6
-rw-r--r--spec/services/members/update_service_spec.rb59
-rw-r--r--spec/services/merge_requests/build_service_spec.rb4
-rw-r--r--spec/services/merge_requests/create_service_spec.rb35
-rw-r--r--spec/services/notes/create_service_spec.rb51
-rw-r--r--spec/services/notes/quick_actions_service_spec.rb30
-rw-r--r--spec/services/projects/update_pages_service_spec.rb16
-rw-r--r--spec/services/projects/update_service_spec.rb34
-rw-r--r--spec/services/system_hooks_service_spec.rb12
-rw-r--r--spec/services/system_note_service_spec.rb6
-rw-r--r--spec/spec_helper.rb4
-rw-r--r--spec/support/bare_repo_operations.rb14
-rw-r--r--spec/support/capybara.rb6
-rw-r--r--spec/support/cluster_application_spec.rb105
-rw-r--r--spec/support/cycle_analytics_helpers.rb49
-rw-r--r--spec/support/db_cleaner.rb4
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb5
-rw-r--r--spec/support/features/variable_list_shared_examples.rb2
-rw-r--r--spec/support/gitlab-git-test.git/packed-refs2
-rw-r--r--spec/support/gitlab_verify.rb45
-rw-r--r--spec/support/ldap_helpers.rb12
-rw-r--r--spec/support/login_helpers.rb6
-rw-r--r--spec/support/shared_examples/models/cluster_application_core_shared_examples.rb70
-rw-r--r--spec/support/shared_examples/models/cluster_application_status_shared_examples.rb31
-rw-r--r--spec/support/slack_mattermost_notifications_shared_examples.rb18
-rw-r--r--spec/tasks/gitlab/check_rake_spec.rb8
-rw-r--r--spec/tasks/gitlab/lfs/check_rake_spec.rb28
-rw-r--r--spec/tasks/gitlab/uploads/check_rake_spec.rb (renamed from spec/tasks/gitlab/uploads_rake_spec.rb)13
-rw-r--r--spec/validators/url_placeholder_validator_spec.rb39
-rw-r--r--spec/validators/url_validator_spec.rb46
-rw-r--r--spec/views/projects/_home_panel.html.haml_spec.rb54
-rw-r--r--spec/workers/authorized_projects_worker_spec.rb79
-rw-r--r--spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb30
-rw-r--r--spec/workers/concerns/gitlab/github_import/object_importer_spec.rb2
-rw-r--r--spec/workers/concerns/waitable_worker_spec.rb92
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb6
-rw-r--r--spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_issue_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_note_worker_spec.rb2
-rw-r--r--spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb2
-rw-r--r--spec/workers/namespaceless_project_destroy_worker_spec.rb4
-rw-r--r--spec/workers/plugin_worker_spec.rb25
-rw-r--r--spec/workers/process_commit_worker_spec.rb74
-rw-r--r--vendor/project_templates/express.tar.gzbin5614 -> 5608 bytes
-rw-r--r--vendor/project_templates/rails.tar.gzbin25007 -> 25004 bytes
-rw-r--r--vendor/project_templates/spring.tar.gzbin50945 -> 50938 bytes
-rw-r--r--vendor/prometheus/values.yaml3
-rw-r--r--vendor/runner/values.yaml23
-rw-r--r--yarn.lock6
1193 files changed, 18833 insertions, 16194 deletions
diff --git a/.codeclimate.yml b/.codeclimate.yml
index b02fe54a4ff..216ecf43beb 100644
--- a/.codeclimate.yml
+++ b/.codeclimate.yml
@@ -11,8 +11,8 @@ engines:
exclude_paths:
- "lib/api/v3/*"
eslint:
- # eslint-plugin-vue is locked to version 2 in codeclimate, we need version 4
- enabled: false
+ enabled: true
+ channel: "eslint-4"
rubocop:
enabled: true
channel: "gitlab-rubocop-0-52-1"
@@ -45,3 +45,4 @@ exclude_paths:
- log/
- backups/
- coverage-javascript/
+- plugins/
diff --git a/.gitignore b/.gitignore
index 2004c2a09b4..fa39ae01ff0 100644
--- a/.gitignore
+++ b/.gitignore
@@ -66,3 +66,4 @@ eslint-report.html
/locale/**/LC_MESSAGES
/locale/**/*.time_stamp
/.rspec
+/plugins/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ae762e7aa6e..8b489f1a07c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,4 +1,4 @@
-image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.14-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3.6-golang-1.9-git-2.16-chrome-63.0-node-8.x-yarn-1.2-postgresql-9.6"
.dedicated-runner: &dedicated-runner
retry: 1
@@ -619,9 +619,10 @@ codequality:
cache: {}
dependencies: []
script:
+ - apk update && apk add jq
- ./scripts/codequality analyze -f json > raw_codeclimate.json || true
# The following line keeps only the fields used in the MR widget, reducing the JSON artifact size
- - cat raw_codeclimate.json | docker run -i stedolan/jq -c 'map({check_name,description,fingerprint,location})' > codeclimate.json
+ - jq -c 'map({check_name,description,fingerprint,location})' raw_codeclimate.json > codeclimate.json
artifacts:
paths: [codeclimate.json]
expire_in: 1 week
diff --git a/.gitlab/issue_templates/Bug.md b/.gitlab/issue_templates/Bug.md
index aec734870d6..3e58d2a867e 100644
--- a/.gitlab/issue_templates/Bug.md
+++ b/.gitlab/issue_templates/Bug.md
@@ -1,3 +1,4 @@
+<!---
Please read this!
Before opening a new issue, make sure to search for keywords in the issues
@@ -14,10 +15,7 @@ For the Enterprise Edition issue tracker:
- https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name%5B%5D=bug
and verify the issue you're about to submit isn't a duplicate.
-
-Please remove this notice if you're confident your issue isn't a duplicate.
-
-------
+--->
### Summary
diff --git a/.rubocop.yml b/.rubocop.yml
index 24edb641657..293f61fb725 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -17,6 +17,7 @@ AllCops:
- 'bin/**/*'
- 'generator_templates/**/*'
- 'builds/**/*'
+ - 'plugins/**/*'
CacheRootDirectory: tmp
# This cop checks whether some constant value isn't a
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 7a12c8473f3..d443238b9e1 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -124,8 +124,8 @@ Lint/DuplicateMethods:
- 'lib/gitlab/git/repository.rb'
- 'lib/gitlab/git/tree.rb'
- 'lib/gitlab/git/wiki_page.rb'
- - 'lib/gitlab/ldap/person.rb'
- - 'lib/gitlab/o_auth/user.rb'
+ - 'lib/gitlab/auth/ldap/person.rb'
+ - 'lib/gitlab/auth/o_auth/user.rb'
# Offense count: 4
Lint/InterpolationCheck:
@@ -812,7 +812,7 @@ Style/TrivialAccessors:
Exclude:
- 'app/models/external_issue.rb'
- 'app/serializers/base_serializer.rb'
- - 'lib/gitlab/ldap/person.rb'
+ - 'lib/gitlab/auth/ldap/person.rb'
- 'lib/system_check/base_check.rb'
# Offense count: 4
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 869884f8ca6..c8d399b2b98 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,27 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 10.5.2 (2018-02-25)
+
+### Fixed (7 changes)
+
+- Fix single digit value clipping for stacked progress bar. !17217
+- Fix issue with cache key being empty when variable used as the key. !17260
+- Enable Legacy Authorization by default on Cluster creations. !17302
+- Allow branch names to be named the same as the sha it points to.
+- Fix 500 error when loading an invalid upload URL.
+- Don't attempt to update user tracked fields if database is in read-only.
+- Prevent MR Widget error when no CI configured.
+
+### Performance (5 changes)
+
+- Improve query performance for snippets dashboard. !17088
+- Only check LFS integrity for first ref in a push to avoid timeout. !17098
+- Improve query performance of MembersFinder. !17190
+- Increase feature flag cache TTL to one hour.
+- Improve performance of searching for and autocompleting of users.
+
+
## 10.5.1 (2018-02-22)
- No changes.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index dfe4bf65f9f..76ee6265c5c 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -196,6 +196,17 @@ release. There are two levels of priority labels:
milestone. If these issues are not done in the current release, they will
strongly be considered for the next release.
+### Severity labels (~S1, ~S2, etc.)
+
+Severity labels help us clearly communicate the impact of a ~bug on users.
+
+| Label | Meaning | Example |
+|-------|------------------------------------------|---------|
+| ~S1 | Feature broken, no workaround | Unable to create an issue |
+| ~S2 | Feature broken, workaround unacceptable | Can push commits, but only via the command line |
+| ~S3 | Feature broken, workaround acceptable | Can create merge requests only from the Merge Requests page, not through the Issue |
+| ~S4 | Cosmetic issue | Label colors are incorrect / not being displayed |
+
### Label for community contributors (~"Accepting Merge Requests")
Issues that are beneficial to our users, 'nice to haves', that we currently do
@@ -397,9 +408,9 @@ For issues related to the open source stewardship of GitLab,
there is the ~"stewardship" label.
This label is to be used for issues in which the stewardship of GitLab
-is a topic of discussion. For instance if GitLab Inc. is planning to remove
-features from GitLab CE to make exclusive in GitLab EE, related issues
-would be labelled with ~"stewardship".
+is a topic of discussion. For instance if GitLab Inc. is planning to add
+features from GitLab EE to GitLab CE, related issues would be labelled with
+~"stewardship".
A recent example of this was the issue for
[bringing the time tracking API to GitLab CE][time-tracking-issue].
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 92fc430ae8f..fe6d01c1a45 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.82.0
+0.88.0
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 40c341bdcdb..19811903a7f 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-3.6.0
+3.8.0
diff --git a/Gemfile b/Gemfile
index 61c129f3036..35f9b081fdf 100644
--- a/Gemfile
+++ b/Gemfile
@@ -411,7 +411,11 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.84.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.88.0', require: 'gitaly'
+# Explicitly lock grpc as we know 1.9 is bad
+# 1.10 is still being tested. See gitlab-org/gitaly#1059
+gem 'grpc', '~> 1.8.3'
+
# Locked until https://github.com/google/protobuf/issues/4210 is closed
gem 'google-protobuf', '= 3.5.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 57ff086f0b1..010d4f7b56a 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -285,7 +285,7 @@ GEM
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
gherkin-ruby (0.3.2)
- gitaly-proto (0.84.0)
+ gitaly-proto (0.88.0)
google-protobuf (~> 3.1)
grpc (~> 1.0)
github-linguist (5.3.3)
@@ -601,7 +601,7 @@ GEM
atomic (>= 1.0.0)
mysql2
peek
- peek-performance_bar (1.3.0)
+ peek-performance_bar (1.3.1)
peek (>= 0.1.0)
peek-pg (1.3.0)
concurrent-ruby
@@ -1057,7 +1057,7 @@ DEPENDENCIES
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.2.0)
- gitaly-proto (~> 0.84.0)
+ gitaly-proto (~> 0.88.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.6.2)
@@ -1073,6 +1073,7 @@ DEPENDENCIES
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
grape_logging (~> 1.7)
+ grpc (~> 1.8.3)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
diff --git a/app/assets/javascripts/autosave.js b/app/assets/javascripts/autosave.js
index 0f28bd233ac..0da872db7e5 100644
--- a/app/assets/javascripts/autosave.js
+++ b/app/assets/javascripts/autosave.js
@@ -3,10 +3,10 @@
import AccessorUtilities from './lib/utils/accessor';
export default class Autosave {
- constructor(field, key, resource) {
+ constructor(field, key) {
this.field = field;
+
this.isLocalStorageAvailable = AccessorUtilities.isLocalStorageAccessSafe();
- this.resource = resource;
if (key.join != null) {
key = key.join('/');
}
@@ -17,31 +17,27 @@ export default class Autosave {
}
restore() {
- var text;
-
if (!this.isLocalStorageAvailable) return;
+ if (!this.field.length) return;
- text = window.localStorage.getItem(this.key);
+ const text = window.localStorage.getItem(this.key);
if ((text != null ? text.length : void 0) > 0) {
this.field.val(text);
}
- if (!this.resource && this.resource !== 'issue') {
- this.field.trigger('input');
- } else {
- // v-model does not update with jQuery trigger
- // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
- const event = new Event('change', { bubbles: true, cancelable: false });
- const field = this.field.get(0);
- if (field) {
- field.dispatchEvent(event);
- }
- }
+
+ this.field.trigger('input');
+ // v-model does not update with jQuery trigger
+ // https://github.com/vuejs/vue/issues/2804#issuecomment-216968137
+ const event = new Event('change', { bubbles: true, cancelable: false });
+ const field = this.field.get(0);
+ field.dispatchEvent(event);
}
save() {
- var text;
- text = this.field.val();
+ if (!this.field.length) return;
+
+ const text = this.field.val();
if (this.isLocalStorageAvailable && (text != null ? text.length : void 0) > 0) {
return window.localStorage.setItem(this.key, text);
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 9456edebccb..26e62732b33 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -2,7 +2,7 @@
import _ from 'underscore';
import Cookies from 'js-cookie';
import { __ } from './locale';
-import { isInIssuePage, updateTooltipTitle } from './lib/utils/common_utils';
+import { isInIssuePage, isInMRPage, hasVueMRDiscussionsCookie, updateTooltipTitle } from './lib/utils/common_utils';
import flash from './flash';
import axios from './lib/utils/axios_utils';
@@ -239,9 +239,9 @@ class AwardsHandler {
}
addAward(votesBlock, awardUrl, emoji, checkMutuality, callback) {
- const isMainAwardsBlock = votesBlock.closest('.js-issue-note-awards').length;
+ const isMainAwardsBlock = votesBlock.closest('.js-noteable-awards').length;
- if (isInIssuePage() && !isMainAwardsBlock) {
+ if (this.isInVueNoteablePage() && !isMainAwardsBlock) {
const id = votesBlock.attr('id').replace('note_', '');
this.hideMenuElement($('.emoji-menu'));
@@ -293,8 +293,16 @@ class AwardsHandler {
}
}
+ isVueMRDiscussions() {
+ return isInMRPage() && hasVueMRDiscussionsCookie() && !$('#diffs').is(':visible');
+ }
+
+ isInVueNoteablePage() {
+ return isInIssuePage() || this.isVueMRDiscussions();
+ }
+
getVotesBlock() {
- if (isInIssuePage()) {
+ if (this.isInVueNoteablePage()) {
const $el = $('.js-add-award.is-active').closest('.note.timeline-entry');
if ($el.length) {
diff --git a/app/assets/javascripts/behaviors/toggler_behavior.js b/app/assets/javascripts/behaviors/toggler_behavior.js
index 417ac31fc86..81c89441424 100644
--- a/app/assets/javascripts/behaviors/toggler_behavior.js
+++ b/app/assets/javascripts/behaviors/toggler_behavior.js
@@ -12,7 +12,7 @@ $(() => {
const $container = $(container);
$container
- .find('.js-toggle-button .fa')
+ .find('.js-toggle-button .fa-chevron-up, .js-toggle-button .fa-chevron-down')
.toggleClass('fa-chevron-up', toggleState)
.toggleClass('fa-chevron-down', toggleState !== undefined ? !toggleState : undefined);
@@ -22,7 +22,7 @@ $(() => {
}
$('body').on('click', '.js-toggle-button', function toggleButton(e) {
- e.target.classList.toggle('open');
+ e.currentTarget.classList.toggle(e.currentTarget.dataset.toggleOpenClass || 'open');
toggleContainer($(this).closest('.js-toggle-container'));
const targetTag = e.currentTarget.tagName.toLowerCase();
diff --git a/app/assets/javascripts/blob/balsamiq_viewer.js b/app/assets/javascripts/blob/balsamiq_viewer.js
index 062577af385..06ef86ecb77 100644
--- a/app/assets/javascripts/blob/balsamiq_viewer.js
+++ b/app/assets/javascripts/blob/balsamiq_viewer.js
@@ -7,7 +7,7 @@ function onError() {
return flash;
}
-function loadBalsamiqFile() {
+export default function loadBalsamiqFile() {
const viewer = document.getElementById('js-balsamiq-viewer');
if (!(viewer instanceof Element)) return;
@@ -17,5 +17,3 @@ function loadBalsamiqFile() {
const balsamiqViewer = new BalsamiqViewer(viewer);
balsamiqViewer.loadFile(endpoint).catch(onError);
}
-
-$(loadBalsamiqFile);
diff --git a/app/assets/javascripts/blob/notebook_viewer.js b/app/assets/javascripts/blob/notebook_viewer.js
index b7a0a195a92..226ae69893e 100644
--- a/app/assets/javascripts/blob/notebook_viewer.js
+++ b/app/assets/javascripts/blob/notebook_viewer.js
@@ -1,3 +1,3 @@
import renderNotebook from './notebook';
-document.addEventListener('DOMContentLoaded', renderNotebook);
+export default renderNotebook;
diff --git a/app/assets/javascripts/blob/pdf_viewer.js b/app/assets/javascripts/blob/pdf_viewer.js
index 91abe9dd699..cabbb396ea7 100644
--- a/app/assets/javascripts/blob/pdf_viewer.js
+++ b/app/assets/javascripts/blob/pdf_viewer.js
@@ -1,3 +1,3 @@
import renderPDF from './pdf';
-document.addEventListener('DOMContentLoaded', renderPDF);
+export default renderPDF;
diff --git a/app/assets/javascripts/blob/sketch_viewer.js b/app/assets/javascripts/blob/sketch_viewer.js
index 0640dd26855..2c1c6339fdb 100644
--- a/app/assets/javascripts/blob/sketch_viewer.js
+++ b/app/assets/javascripts/blob/sketch_viewer.js
@@ -1,8 +1,8 @@
/* eslint-disable no-new */
import SketchLoader from './sketch';
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const el = document.getElementById('js-sketch-viewer');
new SketchLoader(el);
-});
+};
diff --git a/app/assets/javascripts/blob/stl_viewer.js b/app/assets/javascripts/blob/stl_viewer.js
index f611c4fe640..63236b6477f 100644
--- a/app/assets/javascripts/blob/stl_viewer.js
+++ b/app/assets/javascripts/blob/stl_viewer.js
@@ -1,6 +1,6 @@
import Renderer from './3d_viewer';
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const viewer = new Renderer(document.getElementById('js-stl-viewer'));
[].slice.call(document.querySelectorAll('.js-material-changer')).forEach((el) => {
@@ -16,4 +16,4 @@ document.addEventListener('DOMContentLoaded', () => {
viewer.changeObjectMaterials(target.dataset.type);
});
});
-});
+};
diff --git a/app/assets/javascripts/blob/viewer/index.js b/app/assets/javascripts/blob/viewer/index.js
index 612f604e725..92ea91c45a8 100644
--- a/app/assets/javascripts/blob/viewer/index.js
+++ b/app/assets/javascripts/blob/viewer/index.js
@@ -5,6 +5,7 @@ import axios from '../../lib/utils/axios_utils';
export default class BlobViewer {
constructor() {
BlobViewer.initAuxiliaryViewer();
+ BlobViewer.initRichViewer();
this.initMainViewers();
}
@@ -16,6 +17,38 @@ export default class BlobViewer {
BlobViewer.loadViewer(auxiliaryViewer);
}
+ static initRichViewer() {
+ const viewer = document.querySelector('.blob-viewer[data-type="rich"]');
+ if (!viewer || !viewer.dataset.richType) return;
+
+ const initViewer = promise => promise
+ .then(module => module.default(viewer))
+ .catch((error) => {
+ Flash('Error loading file viewer.');
+ throw error;
+ });
+
+ switch (viewer.dataset.richType) {
+ case 'balsamiq':
+ initViewer(import(/* webpackChunkName: 'balsamiq_viewer' */ '../balsamiq_viewer'));
+ break;
+ case 'notebook':
+ initViewer(import(/* webpackChunkName: 'notebook_viewer' */ '../notebook_viewer'));
+ break;
+ case 'pdf':
+ initViewer(import(/* webpackChunkName: 'pdf_viewer' */ '../pdf_viewer'));
+ break;
+ case 'sketch':
+ initViewer(import(/* webpackChunkName: 'sketch_viewer' */ '../sketch_viewer'));
+ break;
+ case 'stl':
+ initViewer(import(/* webpackChunkName: 'stl_viewer' */ '../stl_viewer'));
+ break;
+ default:
+ break;
+ }
+ }
+
initMainViewers() {
this.$fileHolder = $('.file-holder');
if (!this.$fileHolder.length) return;
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 9a0442e2afe..6637904d87d 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -1,6 +1,6 @@
<script>
import Sortable from 'vendor/Sortable';
-import boardNewIssue from './board_new_issue';
+import boardNewIssue from './board_new_issue.vue';
import boardCard from './board_card.vue';
import eventHub from '../eventhub';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js b/app/assets/javascripts/boards/components/board_new_issue.vue
index bc28f7f45f4..efface7143d 100644
--- a/app/assets/javascripts/boards/components/board_new_issue.js
+++ b/app/assets/javascripts/boards/components/board_new_issue.vue
@@ -1,5 +1,6 @@
-/* global ListIssue */
+<script>
import eventHub from '../eventhub';
+import ListIssue from '../models/issue';
const Store = gl.issueBoards.BoardsStore;
@@ -17,6 +18,9 @@ export default {
error: false,
};
},
+ mounted() {
+ this.$refs.input.focus();
+ },
methods: {
submit(e) {
e.preventDefault();
@@ -59,42 +63,51 @@ export default {
eventHub.$emit(`hide-issue-form-${this.list.id}`);
},
},
- mounted() {
- this.$refs.input.focus();
- },
- template: `
- <div class="card board-new-issue-form">
- <form @submit="submit($event)">
- <div class="flash-container"
- v-if="error">
- <div class="flash-alert">
- An error occurred. Please try again.
- </div>
- </div>
- <label class="label-light"
- :for="list.id + '-title'">
- Title
- </label>
- <input class="form-control"
- type="text"
- v-model="title"
- ref="input"
- autocomplete="off"
- :id="list.id + '-title'" />
- <div class="clearfix prepend-top-10">
- <button class="btn btn-success pull-left"
- type="submit"
- :disabled="title === ''"
- ref="submit-button">
- Submit issue
- </button>
- <button class="btn btn-default pull-right"
- type="button"
- @click="cancel">
- Cancel
- </button>
- </div>
- </form>
- </div>
- `,
};
+</script>
+
+<template>
+ <div class="card board-new-issue-form">
+ <form @submit="submit($event)">
+ <div
+ class="flash-container"
+ v-if="error"
+ >
+ <div class="flash-alert">
+ An error occurred. Please try again.
+ </div>
+ </div>
+ <label
+ class="label-light"
+ :for="list.id + '-title'"
+ >
+ Title
+ </label>
+ <input
+ class="form-control"
+ type="text"
+ v-model="title"
+ ref="input"
+ autocomplete="off"
+ :id="list.id + '-title'"
+ />
+ <div class="clearfix prepend-top-10">
+ <button
+ class="btn btn-success pull-left"
+ type="submit"
+ :disabled="title === ''"
+ ref="submit-button"
+ >
+ Submit issue
+ </button>
+ <button
+ class="btn btn-default pull-right"
+ type="button"
+ @click="cancel"
+ >
+ Cancel
+ </button>
+ </div>
+ </form>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/components/board_sidebar.js b/app/assets/javascripts/boards/components/board_sidebar.js
index add24303e7b..9501e35b178 100644
--- a/app/assets/javascripts/boards/components/board_sidebar.js
+++ b/app/assets/javascripts/boards/components/board_sidebar.js
@@ -6,7 +6,7 @@ import { __ } from '../../locale';
import Sidebar from '../../right_sidebar';
import eventHub from '../../sidebar/event_hub';
import assigneeTitle from '../../sidebar/components/assignees/assignee_title';
-import assignees from '../../sidebar/components/assignees/assignees';
+import assignees from '../../sidebar/components/assignees/assignees.vue';
import DueDateSelectors from '../../due_date_select';
import './sidebar/remove_issue';
import IssuableContext from '../../issuable_context';
diff --git a/app/assets/javascripts/boards/filtered_search_boards.js b/app/assets/javascripts/boards/filtered_search_boards.js
index 0df1f7a6f82..57a7cc4ca30 100644
--- a/app/assets/javascripts/boards/filtered_search_boards.js
+++ b/app/assets/javascripts/boards/filtered_search_boards.js
@@ -4,7 +4,9 @@ import FilteredSearchManager from '../filtered_search/filtered_search_manager';
export default class FilteredSearchBoards extends FilteredSearchManager {
constructor(store, updateUrl = false, cantEdit = []) {
- super('boards');
+ super({
+ page: 'boards',
+ });
this.store = store;
this.updateUrl = updateUrl;
diff --git a/app/assets/javascripts/boards/boards_bundle.js b/app/assets/javascripts/boards/index.js
index 90166b3d3d1..8b34fe232c2 100644
--- a/app/assets/javascripts/boards/boards_bundle.js
+++ b/app/assets/javascripts/boards/index.js
@@ -2,13 +2,15 @@
import _ from 'underscore';
import Vue from 'vue';
-import Flash from '../flash';
-import { __ } from '../locale';
+
+import Flash from '~/flash';
+import { __ } from '~/locale';
+import '~/vue_shared/models/label';
+
import FilteredSearchBoards from './filtered_search_boards';
import eventHub from './eventhub';
-import sidebarEventHub from '../sidebar/event_hub';
+import sidebarEventHub from '~/sidebar/event_hub'; // eslint-disable-line import/first
import './models/issue';
-import './models/label';
import './models/list';
import './models/milestone';
import './models/assignee';
@@ -22,9 +24,9 @@ import './components/board';
import './components/board_sidebar';
import './components/new_list_dropdown';
import './components/modal/index';
-import '../vue_shared/vue_resource_interceptor';
+import '~/vue_shared/vue_resource_interceptor'; // eslint-disable-line import/first
-$(() => {
+export default () => {
const $boardApp = document.getElementById('board-app');
const Store = gl.issueBoards.BoardsStore;
const ModalStore = gl.issueBoards.ModalStore;
@@ -236,4 +238,4 @@ $(() => {
</div>
`,
});
-});
+};
diff --git a/app/assets/javascripts/boards/models/issue.js b/app/assets/javascripts/boards/models/issue.js
index 81edd95bf2b..3bfb6d39ad5 100644
--- a/app/assets/javascripts/boards/models/issue.js
+++ b/app/assets/javascripts/boards/models/issue.js
@@ -110,3 +110,5 @@ class ListIssue {
}
window.ListIssue = ListIssue;
+
+export default ListIssue;
diff --git a/app/assets/javascripts/boards/stores/boards_store.js b/app/assets/javascripts/boards/stores/boards_store.js
index 798d7e0d147..348cdeec737 100644
--- a/app/assets/javascripts/boards/stores/boards_store.js
+++ b/app/assets/javascripts/boards/stores/boards_store.js
@@ -2,7 +2,7 @@
/* global List */
import _ from 'underscore';
import Cookies from 'js-cookie';
-import { getUrlParamsArray } from '../../lib/utils/common_utils';
+import { getUrlParamsArray } from '~/lib/utils/common_utils';
window.gl = window.gl || {};
window.gl.issueBoards = window.gl.issueBoards || {};
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index b070a59cf15..01aec4f36af 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -37,10 +37,11 @@ export default class Clusters {
clusterStatusReason,
helpPath,
ingressHelpPath,
+ ingressDnsHelpPath,
} = document.querySelector('.js-edit-cluster-form').dataset;
this.store = new ClustersStore();
- this.store.setHelpPaths(helpPath, ingressHelpPath);
+ this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
this.store.updateStatus(clusterStatus);
this.store.updateStatusReason(clusterStatusReason);
@@ -98,6 +99,7 @@ export default class Clusters {
helpPath: this.state.helpPath,
ingressHelpPath: this.state.ingressHelpPath,
managePrometheusPath: this.state.managePrometheusPath,
+ ingressDnsHelpPath: this.state.ingressDnsHelpPath,
},
});
},
diff --git a/app/assets/javascripts/clusters/components/application_row.vue b/app/assets/javascripts/clusters/components/application_row.vue
index 50e35bbbba5..c2a35341eb2 100644
--- a/app/assets/javascripts/clusters/components/application_row.vue
+++ b/app/assets/javascripts/clusters/components/application_row.vue
@@ -36,10 +36,6 @@
type: String,
required: false,
},
- description: {
- type: String,
- required: true,
- },
status: {
type: String,
required: false,
@@ -148,7 +144,7 @@
class="table-section section-wrap"
role="gridcell"
>
- <div v-html="description"></div>
+ <slot name="description"></slot>
</div>
<div
class="table-section table-button-footer section-align-top"
diff --git a/app/assets/javascripts/clusters/components/applications.vue b/app/assets/javascripts/clusters/components/applications.vue
index 978881a4831..1325a268214 100644
--- a/app/assets/javascripts/clusters/components/applications.vue
+++ b/app/assets/javascripts/clusters/components/applications.vue
@@ -2,10 +2,16 @@
import _ from 'underscore';
import { s__, sprintf } from '../../locale';
import applicationRow from './application_row.vue';
+ import clipboardButton from '../../vue_shared/components/clipboard_button.vue';
+ import {
+ APPLICATION_INSTALLED,
+ INGRESS,
+ } from '../constants';
export default {
components: {
applicationRow,
+ clipboardButton,
},
props: {
applications: {
@@ -23,6 +29,11 @@
required: false,
default: '',
},
+ ingressDnsHelpPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
managePrometheusPath: {
type: String,
required: false,
@@ -43,19 +54,16 @@
false,
);
},
- helmTillerDescription() {
- return _.escape(s__(
- `ClusterIntegration|Helm streamlines installing and managing Kubernetes applications.
- Tiller runs inside of your Kubernetes Cluster, and manages
- releases of your charts.`,
- ));
+ ingressId() {
+ return INGRESS;
+ },
+ ingressInstalled() {
+ return this.applications.ingress.status === APPLICATION_INSTALLED;
+ },
+ ingressExternalIp() {
+ return this.applications.ingress.externalIp;
},
ingressDescription() {
- const descriptionParagraph = _.escape(s__(
- `ClusterIntegration|Ingress gives you a way to route requests to services based on the
- request host or path, centralizing a number of services into a single entrypoint.`,
- ));
-
const extraCostParagraph = sprintf(
_.escape(s__(
`ClusterIntegration|%{boldNotice} This will add some extra resources
@@ -84,9 +92,6 @@
return `
<p>
- ${descriptionParagraph}
- </p>
- <p>
${extraCostParagraph}
</p>
<p class="settings-message append-bottom-0">
@@ -94,12 +99,6 @@
</p>
`;
},
- gitlabRunnerDescription() {
- return _.escape(s__(
- `ClusterIntegration|GitLab Runner is the open source project that is used to run your jobs
- and send the results back to GitLab.`,
- ));
- },
prometheusDescription() {
return sprintf(
_.escape(s__(
@@ -136,33 +135,137 @@
id="helm"
:title="applications.helm.title"
title-link="https://docs.helm.sh/"
- :description="helmTillerDescription"
:status="applications.helm.status"
:status-reason="applications.helm.statusReason"
:request-status="applications.helm.requestStatus"
:request-reason="applications.helm.requestReason"
- />
+ >
+ <div slot="description">
+ {{ s__(`ClusterIntegration|Helm streamlines installing
+ and managing Kubernetes applications.
+ Tiller runs inside of your Kubernetes Cluster,
+ and manages releases of your charts.`) }}
+ </div>
+ </application-row>
<application-row
- id="ingress"
+ :id="ingressId"
:title="applications.ingress.title"
title-link="https://kubernetes.io/docs/concepts/services-networking/ingress/"
- :description="ingressDescription"
:status="applications.ingress.status"
:status-reason="applications.ingress.statusReason"
:request-status="applications.ingress.requestStatus"
:request-reason="applications.ingress.requestReason"
- />
+ >
+ <div slot="description">
+ <p>
+ {{ s__(`ClusterIntegration|Ingress gives you a way to route
+ requests to services based on the request host or path,
+ centralizing a number of services into a single entrypoint.`) }}
+ </p>
+
+ <template v-if="ingressInstalled">
+ <div class="form-group">
+ <label for="ingress-ip-address">
+ {{ s__('ClusterIntegration|Ingress IP Address') }}
+ </label>
+ <div
+ v-if="ingressExternalIp"
+ class="input-group"
+ >
+ <input
+ type="text"
+ id="ingress-ip-address"
+ class="form-control js-ip-address"
+ :value="ingressExternalIp"
+ readonly
+ />
+ <span class="input-group-btn">
+ <clipboard-button
+ :text="ingressExternalIp"
+ :title="s__('ClusterIntegration|Copy Ingress IP Address to clipboard')"
+ css-class="btn btn-default js-clipboard-btn"
+ />
+ </span>
+ </div>
+ <input
+ v-else
+ type="text"
+ class="form-control js-ip-address"
+ readonly
+ value="?"
+ />
+ </div>
+
+ <p
+ v-if="!ingressExternalIp"
+ class="settings-message js-no-ip-message"
+ >
+ {{ s__(`ClusterIntegration|The IP address is in
+ the process of being assigned. Please check your Kubernetes
+ cluster or Quotas on GKE if it takes a long time.`) }}
+
+ <a
+ :href="ingressHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ <p>
+ {{ s__(`ClusterIntegration|Point a wildcard DNS to this
+ generated IP address in order to access
+ your application after it has been deployed.`) }}
+ <a
+ :href="ingressDnsHelpPath"
+ target="_blank"
+ rel="noopener noreferrer"
+ >
+ {{ __('More information') }}
+ </a>
+ </p>
+
+ </template>
+ <div
+ v-else
+ v-html="ingressDescription"
+ >
+ </div>
+ </div>
+ </application-row>
<application-row
id="prometheus"
:title="applications.prometheus.title"
title-link="https://prometheus.io/docs/introduction/overview/"
:manage-link="managePrometheusPath"
- :description="prometheusDescription"
:status="applications.prometheus.status"
:status-reason="applications.prometheus.statusReason"
:request-status="applications.prometheus.requestStatus"
:request-reason="applications.prometheus.requestReason"
- />
+ >
+ <div
+ slot="description"
+ v-html="prometheusDescription"
+ >
+ </div>
+ </application-row>
+ <application-row
+ id="runner"
+ :title="applications.runner.title"
+ title-link="https://docs.gitlab.com/runner/"
+ :status="applications.runner.status"
+ :status-reason="applications.runner.statusReason"
+ :request-status="applications.runner.requestStatus"
+ :request-reason="applications.runner.requestReason"
+ >
+ <div slot="description">
+ {{ s__(`ClusterIntegration|GitLab Runner connects to this
+ project's repository and executes CI/CD jobs,
+ pushing results back and deploying,
+ applications to production.`) }}
+ </div>
+ </application-row>
<!--
NOTE: Don't forget to update `clusters.scss`
min-height for this block and uncomment `application_spec` tests
diff --git a/app/assets/javascripts/clusters/constants.js b/app/assets/javascripts/clusters/constants.js
index 93223aefff8..b7179f52bb3 100644
--- a/app/assets/javascripts/clusters/constants.js
+++ b/app/assets/javascripts/clusters/constants.js
@@ -10,3 +10,4 @@ export const APPLICATION_ERROR = 'errored';
export const REQUEST_LOADING = 'request-loading';
export const REQUEST_SUCCESS = 'request-success';
export const REQUEST_FAILURE = 'request-failure';
+export const INGRESS = 'ingress';
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index 904ee5fd475..348bbec3b25 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -1,4 +1,5 @@
import { s__ } from '../../locale';
+import { INGRESS } from '../constants';
export default class ClusterStore {
constructor() {
@@ -21,6 +22,7 @@ export default class ClusterStore {
statusReason: null,
requestStatus: null,
requestReason: null,
+ externalIp: null,
},
runner: {
title: s__('ClusterIntegration|GitLab Runner'),
@@ -40,9 +42,10 @@ export default class ClusterStore {
};
}
- setHelpPaths(helpPath, ingressHelpPath) {
+ setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath) {
this.state.helpPath = helpPath;
this.state.ingressHelpPath = ingressHelpPath;
+ this.state.ingressDnsHelpPath = ingressDnsHelpPath;
}
setManagePrometheusPath(managePrometheusPath) {
@@ -64,6 +67,7 @@ export default class ClusterStore {
updateStateFromServer(serverState = {}) {
this.state.status = serverState.status;
this.state.statusReason = serverState.status_reason;
+
serverState.applications.forEach((serverAppEntry) => {
const {
name: appId,
@@ -76,6 +80,10 @@ export default class ClusterStore {
status,
statusReason,
};
+
+ if (appId === INGRESS) {
+ this.state.applications.ingress.externalIp = serverAppEntry.external_ip;
+ }
});
}
}
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 1f9153d95bd..3d89bf1316e 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -15,7 +15,7 @@ const CommitPipelinesTable = Vue.extend(commitPipelinesTable);
window.gl = window.gl || {};
window.gl.CommitPipelinesTable = CommitPipelinesTable;
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const pipelineTableViewEl = document.querySelector('#commit-pipeline-table-view');
if (pipelineTableViewEl) {
@@ -43,4 +43,4 @@ document.addEventListener('DOMContentLoaded', () => {
pipelineTableViewEl.appendChild(table.$el);
}
}
-});
+};
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index ce19069f103..466a5b5d635 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -20,10 +20,6 @@
type: String,
required: true,
},
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
errorStateSvgPath: {
type: String,
required: true,
@@ -45,23 +41,14 @@
},
computed: {
- /**
- * Empty state is only rendered if after the first request we receive no pipelines.
- *
- * @return {Boolean}
- */
- shouldRenderEmptyState() {
- return !this.state.pipelines.length &&
- !this.isLoading &&
- this.hasMadeRequest &&
- !this.hasError;
- },
-
shouldRenderTable() {
return !this.isLoading &&
this.state.pipelines.length > 0 &&
!this.hasError;
},
+ shouldRenderErrorState() {
+ return this.hasError && !this.isLoading;
+ },
},
created() {
this.service = new PipelinesService(this.endpoint);
@@ -92,25 +79,22 @@
<div class="content-list pipelines">
<loading-icon
- label="Loading pipelines"
+ :label="s__('Pipelines|Loading Pipelines')"
size="3"
v-if="isLoading"
+ class="prepend-top-20"
/>
- <empty-state
- v-if="shouldRenderEmptyState"
- :help-page-path="helpPagePath"
- :empty-state-svg-path="emptyStateSvgPath"
- />
-
- <error-state
- v-if="shouldRenderErrorState"
- :error-state-svg-path="errorStateSvgPath"
+ <svg-blank-state
+ v-else-if="shouldRenderErrorState"
+ :svg-path="errorStateSvgPath"
+ :message="s__(`Pipelines|There was an error fetching the pipelines.
+ Try again in a few moments or contact your support team.`)"
/>
<div
class="table-holder"
- v-if="shouldRenderTable"
+ v-else-if="shouldRenderTable"
>
<pipelines-table-component
:pipelines="state.pipelines"
diff --git a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
index 034f2923b3b..46d89c825f9 100644
--- a/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
+++ b/app/assets/javascripts/cycle_analytics/cycle_analytics_bundle.js
@@ -14,10 +14,10 @@ import CycleAnalyticsStore from './cycle_analytics_store';
Vue.use(Translate);
-$(() => {
+export default () => {
const OVERVIEW_DIALOG_COOKIE = 'cycle_analytics_help_dismissed';
- gl.cycleAnalyticsApp = new Vue({
+ new Vue({ // eslint-disable-line no-new
el: '#cycle-analytics',
name: 'CycleAnalytics',
components: {
@@ -132,4 +132,4 @@ $(() => {
},
},
});
-});
+};
diff --git a/app/assets/javascripts/deploy_keys/index.js b/app/assets/javascripts/deploy_keys/index.js
index ca8798facc9..b727261648c 100644
--- a/app/assets/javascripts/deploy_keys/index.js
+++ b/app/assets/javascripts/deploy_keys/index.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import deployKeysApp from './components/app.vue';
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: document.getElementById('js-deploy-keys'),
components: {
deployKeysApp,
@@ -18,4 +18,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index e77910a83d4..fadc34959e1 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -197,7 +197,7 @@ const JumpToDiscussion = Vue.extend({
}
$.scrollTo($target, {
- offset: 0
+ offset: -150
});
}
},
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js b/app/assets/javascripts/diff_notes/components/resolve_btn.js
index 20ddcbfb8bd..cc9192deae3 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js
@@ -87,6 +87,7 @@ const ResolveBtn = Vue.extend({
CommentsStore.update(this.discussionId, this.noteId, !this.isResolved, resolved_by);
this.discussion.updateHeadline(data);
gl.mrWidget.checkStatus();
+ document.dispatchEvent(new CustomEvent('refreshVueNotes'));
this.updateTooltip();
})
diff --git a/app/assets/javascripts/diff_notes/diff_notes_bundle.js b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
index 679057e787c..5f49609fe88 100644
--- a/app/assets/javascripts/diff_notes/diff_notes_bundle.js
+++ b/app/assets/javascripts/diff_notes/diff_notes_bundle.js
@@ -14,6 +14,7 @@ import './components/resolve_count';
import './components/resolve_discussion_btn';
import './components/diff_note_avatars';
import './components/new_issue_for_discussion';
+import { hasVueMRDiscussionsCookie } from '../lib/utils/common_utils';
export default () => {
const projectPathHolder = document.querySelector('.merge-request') || document.querySelector('.commit-box');
@@ -67,12 +68,14 @@ export default () => {
gl.diffNotesCompileComponents();
- new Vue({
- el: '#resolve-count-app',
- components: {
- 'resolve-count': ResolveCount
- },
- });
+ if (!hasVueMRDiscussionsCookie()) {
+ new Vue({
+ el: '#resolve-count-app',
+ components: {
+ 'resolve-count': ResolveCount
+ },
+ });
+ }
$(window).trigger('resize.nav');
};
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js b/app/assets/javascripts/diff_notes/services/resolve.js
index 96fe23640af..d16f9297de1 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js
+++ b/app/assets/javascripts/diff_notes/services/resolve.js
@@ -8,8 +8,8 @@ window.gl = window.gl || {};
class ResolveServiceClass {
constructor(root) {
- this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve`);
- this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve`);
+ this.noteResource = Vue.resource(`${root}/notes{/noteId}/resolve?html=true`);
+ this.discussionResource = Vue.resource(`${root}/merge_requests{/mergeRequestId}/discussions{/discussionId}/resolve?html=true`);
}
resolve(noteId) {
@@ -45,6 +45,7 @@ class ResolveServiceClass {
if (gl.mrWidget) gl.mrWidget.checkStatus();
discussion.updateHeadline(data);
+ document.dispatchEvent(new CustomEvent('refreshVueNotes'));
})
.catch(() => new Flash('An error occurred when trying to resolve a discussion. Please try again.'));
}
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index acf0effa00d..1ccf96a75dc 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -6,177 +6,80 @@ import GlFieldErrors from './gl_field_errors';
import Shortcuts from './shortcuts';
import SearchAutocomplete from './search_autocomplete';
-var Dispatcher;
-
-(function() {
- Dispatcher = (function() {
- function Dispatcher() {
- this.initSearch();
- this.initFieldErrors();
- this.initPageScripts();
- }
-
- Dispatcher.prototype.initPageScripts = function() {
- var path, shortcut_handler;
- const page = $('body').attr('data-page');
- if (!page) {
- return false;
- }
-
- const fail = () => Flash('Error loading dynamic module');
- const callDefault = m => m.default();
-
- path = page.split(':');
- shortcut_handler = null;
-
- $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
- const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
- const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
- gfm.setup($(el), {
- emojis: true,
- members: enableGFM,
- issues: enableGFM,
- milestones: enableGFM,
- mergeRequests: enableGFM,
- labels: enableGFM,
- });
- });
-
- const shortcutHandlerPages = [
- 'projects:activity',
- 'projects:artifacts:browse',
- 'projects:artifacts:file',
- 'projects:blame:show',
- 'projects:blob:show',
- 'projects:commit:show',
- 'projects:commits:show',
- 'projects:find_file:show',
- 'projects:issues:edit',
- 'projects:issues:index',
- 'projects:issues:new',
- 'projects:issues:show',
- 'projects:merge_requests:creations:diffs',
- 'projects:merge_requests:creations:new',
- 'projects:merge_requests:edit',
- 'projects:merge_requests:index',
- 'projects:merge_requests:show',
- 'projects:network:show',
- 'projects:show',
- 'projects:tree:show',
- 'groups:show',
- ];
+function initSearch() {
+ // Only when search form is present
+ if ($('.search').length) {
+ return new SearchAutocomplete();
+ }
+}
- if (shortcutHandlerPages.indexOf(page) !== -1) {
- shortcut_handler = true;
- }
+function initFieldErrors() {
+ $('.gl-show-field-errors').each((i, form) => {
+ new GlFieldErrors(form);
+ });
+}
- switch (path[0]) {
- case 'admin':
- switch (path[1]) {
- case 'broadcast_messages':
- import('./pages/admin/broadcast_messages')
- .then(callDefault)
- .catch(fail);
- break;
- case 'cohorts':
- import('./pages/admin/cohorts')
- .then(callDefault)
- .catch(fail);
- break;
- case 'groups':
- switch (path[2]) {
- case 'show':
- import('./pages/admin/groups/show')
- .then(callDefault)
- .catch(fail);
- break;
- }
- break;
- case 'projects':
- import('./pages/admin/projects')
- .then(callDefault)
- .catch(fail);
- break;
- case 'labels':
- switch (path[2]) {
- case 'new':
- import('./pages/admin/labels/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'edit':
- import('./pages/admin/labels/edit')
- .then(callDefault)
- .catch(fail);
- break;
- }
- case 'abuse_reports':
- import('./pages/admin/abuse_reports')
- .then(callDefault)
- .catch(fail);
- break;
- }
- break;
- case 'profiles':
- import('./pages/profiles/index')
- .then(callDefault)
- .catch(fail);
- break;
- case 'projects':
- import('./pages/projects')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- switch (path[1]) {
- case 'compare':
- import('./pages/projects/compare')
- .then(callDefault)
- .catch(fail);
- break;
- case 'create':
- case 'new':
- import('./pages/projects/new')
- .then(callDefault)
- .catch(fail);
- break;
- case 'wikis':
- import('./pages/projects/wikis')
- .then(callDefault)
- .catch(fail);
- shortcut_handler = true;
- break;
- }
- break;
- }
- // If we haven't installed a custom shortcut handler, install the default one
- if (!shortcut_handler) {
- new Shortcuts();
- }
+function initPageShortcuts(page) {
+ const pagesWithCustomShortcuts = [
+ 'projects:activity',
+ 'projects:artifacts:browse',
+ 'projects:artifacts:file',
+ 'projects:blame:show',
+ 'projects:blob:show',
+ 'projects:commit:show',
+ 'projects:commits:show',
+ 'projects:find_file:show',
+ 'projects:issues:edit',
+ 'projects:issues:index',
+ 'projects:issues:new',
+ 'projects:issues:show',
+ 'projects:merge_requests:creations:diffs',
+ 'projects:merge_requests:creations:new',
+ 'projects:merge_requests:edit',
+ 'projects:merge_requests:index',
+ 'projects:merge_requests:show',
+ 'projects:network:show',
+ 'projects:show',
+ 'projects:tree:show',
+ 'groups:show',
+ ];
- if (document.querySelector('#peek')) {
- import('./performance_bar')
- .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
- .catch(fail);
- }
- };
+ if (pagesWithCustomShortcuts.indexOf(page) === -1) {
+ new Shortcuts();
+ }
+}
- Dispatcher.prototype.initSearch = function() {
- // Only when search form is present
- if ($('.search').length) {
- return new SearchAutocomplete();
- }
- };
+function initGFMInput() {
+ $('.js-gfm-input:not(.js-vue-textarea)').each((i, el) => {
+ const gfm = new GfmAutoComplete(gl.GfmAutoComplete && gl.GfmAutoComplete.dataSources);
+ const enableGFM = convertPermissionToBoolean(el.dataset.supportsAutocomplete);
+ gfm.setup($(el), {
+ emojis: true,
+ members: enableGFM,
+ issues: enableGFM,
+ milestones: enableGFM,
+ mergeRequests: enableGFM,
+ labels: enableGFM,
+ });
+ });
+}
- Dispatcher.prototype.initFieldErrors = function() {
- $('.gl-show-field-errors').each((i, form) => {
- new GlFieldErrors(form);
- });
- };
+function initPerformanceBar() {
+ if (document.querySelector('#peek')) {
+ import('./performance_bar')
+ .then(m => new m.default({ container: '#peek' })) // eslint-disable-line new-cap
+ .catch(() => Flash('Error loading performance bar module'));
+ }
+}
- return Dispatcher;
- })();
-})();
+export default () => {
+ initSearch();
+ initFieldErrors();
-export default function initDispatcher() {
- return new Dispatcher();
-}
+ const page = $('body').attr('data-page');
+ if (page) {
+ initPageShortcuts(page);
+ initGFMInput();
+ initPerformanceBar();
+ }
+};
diff --git a/app/assets/javascripts/environments/components/environments_table.vue b/app/assets/javascripts/environments/components/environments_table.vue
index b4eca47957e..22863e926d4 100644
--- a/app/assets/javascripts/environments/components/environments_table.vue
+++ b/app/assets/javascripts/environments/components/environments_table.vue
@@ -2,8 +2,8 @@
/**
* Render environments table.
*/
+import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import environmentItem from './environment_item.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
export default {
components: {
diff --git a/app/assets/javascripts/environments/folder/environments_folder_bundle.js b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
index 5d2d14c7682..de0fbdb2e91 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_bundle.js
+++ b/app/assets/javascripts/environments/folder/environments_folder_bundle.js
@@ -5,7 +5,7 @@ import Translate from '../../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#environments-folder-list-view',
components: {
environmentsFolderApp,
@@ -32,4 +32,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/environments/environments_bundle.js b/app/assets/javascripts/environments/index.js
index 2e0a4001b7c..afc4aba6554 100644
--- a/app/assets/javascripts/environments/environments_bundle.js
+++ b/app/assets/javascripts/environments/index.js
@@ -5,7 +5,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#environments-list-view',
components: {
environmentsComponent,
@@ -36,4 +36,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js
deleted file mode 100644
index b693084e434..00000000000
--- a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.js
+++ /dev/null
@@ -1,102 +0,0 @@
-import eventHub from '../event_hub';
-import FilteredSearchTokenizer from '../filtered_search_tokenizer';
-
-export default {
- name: 'RecentSearchesDropdownContent',
-
- props: {
- items: {
- type: Array,
- required: true,
- },
- isLocalStorageAvailable: {
- type: Boolean,
- required: false,
- default: true,
- },
- allowedKeys: {
- type: Array,
- required: true,
- },
- },
-
- computed: {
- processedItems() {
- return this.items.map((item) => {
- const { tokens, searchToken }
- = FilteredSearchTokenizer.processTokens(item, this.allowedKeys);
-
- const resultantTokens = tokens.map(token => ({
- prefix: `${token.key}:`,
- suffix: `${token.symbol}${token.value}`,
- }));
-
- return {
- text: item,
- tokens: resultantTokens,
- searchToken,
- };
- });
- },
- hasItems() {
- return this.items.length > 0;
- },
- },
-
- methods: {
- onItemActivated(text) {
- eventHub.$emit('recentSearchesItemSelected', text);
- },
- onRequestClearRecentSearches(e) {
- // Stop the dropdown from closing
- e.stopPropagation();
-
- eventHub.$emit('requestClearRecentSearches');
- },
- },
-
- template: `
- <div>
- <div
- v-if="!isLocalStorageAvailable"
- class="dropdown-info-note">
- This feature requires local storage to be enabled
- </div>
- <ul v-else-if="hasItems">
- <li
- v-for="(item, index) in processedItems"
- :key="index">
- <button
- type="button"
- class="filtered-search-history-dropdown-item"
- @click="onItemActivated(item.text)">
- <span>
- <span
- v-for="(token, tokenIndex) in item.tokens"
- class="filtered-search-history-dropdown-token">
- <span class="name">{{ token.prefix }}</span><span class="value">{{ token.suffix }}</span>
- </span>
- </span>
- <span class="filtered-search-history-dropdown-search-token">
- {{ item.searchToken }}
- </span>
- </button>
- </li>
- <li class="divider"></li>
- <li>
- <button
- type="button"
- class="filtered-search-history-clear-button"
- @click="onRequestClearRecentSearches($event)">
- Clear recent searches
- </button>
- </li>
- </ul>
- <div
- v-else
- class="dropdown-info-note">
- You don't have any recent searches
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
new file mode 100644
index 00000000000..26618af9515
--- /dev/null
+++ b/app/assets/javascripts/filtered_search/components/recent_searches_dropdown_content.vue
@@ -0,0 +1,104 @@
+<script>
+import eventHub from '../event_hub';
+import FilteredSearchTokenizer from '../filtered_search_tokenizer';
+
+export default {
+ name: 'RecentSearchesDropdownContent',
+ props: {
+ items: {
+ type: Array,
+ required: true,
+ },
+ isLocalStorageAvailable: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
+ allowedKeys: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ processedItems() {
+ return this.items.map((item) => {
+ const { tokens, searchToken }
+ = FilteredSearchTokenizer.processTokens(item, this.allowedKeys);
+
+ const resultantTokens = tokens.map(token => ({
+ prefix: `${token.key}:`,
+ suffix: `${token.symbol}${token.value}`,
+ }));
+
+ return {
+ text: item,
+ tokens: resultantTokens,
+ searchToken,
+ };
+ });
+ },
+ hasItems() {
+ return this.items.length > 0;
+ },
+ },
+ methods: {
+ onItemActivated(text) {
+ eventHub.$emit('recentSearchesItemSelected', text);
+ },
+ onRequestClearRecentSearches(e) {
+ // Stop the dropdown from closing
+ e.stopPropagation();
+
+ eventHub.$emit('requestClearRecentSearches');
+ },
+ },
+};
+</script>
+<template>
+ <div>
+ <div
+ v-if="!isLocalStorageAvailable"
+ class="dropdown-info-note">
+ This feature requires local storage to be enabled
+ </div>
+ <ul v-else-if="hasItems">
+ <li
+ v-for="(item, index) in processedItems"
+ :key="`processed-items-${index}`"
+ >
+ <button
+ type="button"
+ class="filtered-search-history-dropdown-item"
+ @click="onItemActivated(item.text)">
+ <span>
+ <span
+ class="filtered-search-history-dropdown-token"
+ v-for="(token, index) in item.tokens"
+ :key="`dropdown-token-${index}`"
+ >
+ <span class="name">{{ token.prefix }}</span>
+ <span class="value">{{ token.suffix }}</span>
+ </span>
+ </span>
+ <span class="filtered-search-history-dropdown-search-token">
+ {{ item.searchToken }}
+ </span>
+ </button>
+ </li>
+ <li class="divider"></li>
+ <li>
+ <button
+ type="button"
+ class="filtered-search-history-clear-button"
+ @click="onRequestClearRecentSearches($event)">
+ Clear recent searches
+ </button>
+ </li>
+ </ul>
+ <div
+ v-else
+ class="dropdown-info-note">
+ You don't have any recent searches
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/filtered_search/filtered_search_bundle.js b/app/assets/javascripts/filtered_search/filtered_search_bundle.js
deleted file mode 100644
index 293154917fa..00000000000
--- a/app/assets/javascripts/filtered_search/filtered_search_bundle.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import './dropdown_emoji';
-import './dropdown_hint';
-import './dropdown_non_user';
-import './dropdown_user';
-import './dropdown_utils';
-import './filtered_search_dropdown_manager';
-import './filtered_search_dropdown';
-import './filtered_search_manager';
-import './filtered_search_tokenizer';
-import './filtered_search_visual_tokens';
diff --git a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
index c64553a1b92..e6390f0855b 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_dropdown_manager.js
@@ -10,13 +10,24 @@ import DropdownUser from './dropdown_user';
import FilteredSearchVisualTokens from './filtered_search_visual_tokens';
export default class FilteredSearchDropdownManager {
- constructor(baseEndpoint = '', tokenizer, page, isGroup, filteredSearchTokenKeys) {
+ constructor({
+ baseEndpoint = '',
+ tokenizer,
+ page,
+ isGroup,
+ isGroupAncestor,
+ isGroupDecendent,
+ filteredSearchTokenKeys,
+ }) {
this.container = FilteredSearchContainer.container;
this.baseEndpoint = baseEndpoint.replace(/\/$/, '');
this.tokenizer = tokenizer;
this.filteredSearchTokenKeys = filteredSearchTokenKeys || FilteredSearchTokenKeys;
this.filteredSearchInput = this.container.querySelector('.filtered-search');
this.page = page;
+ this.groupsOnly = isGroup;
+ this.groupAncestor = isGroupAncestor;
+ this.isGroupDecendent = isGroupDecendent;
this.setupMapping();
@@ -59,7 +70,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
- endpoint: `${this.baseEndpoint}/milestones.json`,
+ endpoint: this.getMilestoneEndpoint(),
symbol: '%',
},
element: this.container.querySelector('#js-dropdown-milestone'),
@@ -68,7 +79,7 @@ export default class FilteredSearchDropdownManager {
reference: null,
gl: DropdownNonUser,
extraArguments: {
- endpoint: `${this.baseEndpoint}/labels.json`,
+ endpoint: this.getLabelsEndpoint(),
symbol: '~',
preprocessing: DropdownUtils.duplicateLabelPreprocessing,
},
@@ -90,6 +101,18 @@ export default class FilteredSearchDropdownManager {
this.mapping = allowedMappings;
}
+ getMilestoneEndpoint() {
+ const endpoint = `${this.baseEndpoint}/milestones.json`;
+
+ return endpoint;
+ }
+
+ getLabelsEndpoint() {
+ const endpoint = `${this.baseEndpoint}/labels.json`;
+
+ return endpoint;
+ }
+
static addWordToInput(tokenName, tokenValue = '', clicked = false) {
const input = FilteredSearchContainer.container.querySelector('.filtered-search');
diff --git a/app/assets/javascripts/filtered_search/filtered_search_manager.js b/app/assets/javascripts/filtered_search/filtered_search_manager.js
index e294b629bd0..71b7e80335b 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_manager.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_manager.js
@@ -20,10 +20,15 @@ import DropdownUtils from './dropdown_utils';
export default class FilteredSearchManager {
constructor({
page,
+ isGroup = false,
+ isGroupAncestor = false,
+ isGroupDecendent = false,
filteredSearchTokenKeys = FilteredSearchTokenKeys,
stateFiltersSelector = '.issues-state-filters',
}) {
- this.isGroup = false;
+ this.isGroup = isGroup;
+ this.isGroupAncestor = isGroupAncestor;
+ this.isGroupDecendent = isGroupDecendent;
this.states = ['opened', 'closed', 'merged', 'all'];
this.page = page;
@@ -75,13 +80,14 @@ export default class FilteredSearchManager {
if (this.filteredSearchInput) {
this.tokenizer = FilteredSearchTokenizer;
- this.dropdownManager = new FilteredSearchDropdownManager(
- this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
- this.tokenizer,
- this.page,
- this.isGroup,
- this.filteredSearchTokenKeys,
- );
+ this.dropdownManager = new FilteredSearchDropdownManager({
+ baseEndpoint: this.filteredSearchInput.getAttribute('data-base-endpoint') || '',
+ tokenizer: this.tokenizer,
+ page: this.page,
+ isGroup: this.isGroup,
+ isGroupAncestor: this.isGroupAncestor,
+ filteredSearchTokenKeys: this.filteredSearchTokenKeys,
+ });
this.recentSearchesRoot = new RecentSearchesRoot(
this.recentSearchesStore,
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index a19bb882410..600024c21c3 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,5 +1,6 @@
import _ from 'underscore';
-import AjaxCache from '../lib/utils/ajax_cache';
+import AjaxCache from '~/lib/utils/ajax_cache';
+import { objectToQueryString } from '~/lib/utils/common_utils';
import Flash from '../flash';
import FilteredSearchContainer from './container';
import UsersCache from '../lib/utils/users_cache';
@@ -16,6 +17,21 @@ export default class FilteredSearchVisualTokens {
};
}
+ /**
+ * Returns a computed API endpoint
+ * and query string composed of values from endpointQueryParams
+ * @param {String} endpoint
+ * @param {String} endpointQueryParams
+ */
+ static getEndpointWithQueryParams(endpoint, endpointQueryParams) {
+ if (!endpointQueryParams) {
+ return endpoint;
+ }
+
+ const queryString = objectToQueryString(JSON.parse(endpointQueryParams));
+ return `${endpoint}?${queryString}`;
+ }
+
static unselectTokens() {
const otherTokens = FilteredSearchContainer.container.querySelectorAll('.js-visual-token .selectable.selected');
[].forEach.call(otherTokens, t => t.classList.remove('selected'));
@@ -86,7 +102,10 @@ export default class FilteredSearchVisualTokens {
static updateLabelTokenColor(tokenValueContainer, tokenValue) {
const filteredSearchInput = FilteredSearchContainer.container.querySelector('.filtered-search');
const baseEndpoint = filteredSearchInput.dataset.baseEndpoint;
- const labelsEndpoint = `${baseEndpoint}/labels.json`;
+ const labelsEndpoint = FilteredSearchVisualTokens.getEndpointWithQueryParams(
+ `${baseEndpoint}/labels.json`,
+ filteredSearchInput.dataset.endpointQueryParams,
+ );
return AjaxCache.retrieve(labelsEndpoint)
.then(FilteredSearchVisualTokens.preprocessLabel.bind(null, labelsEndpoint))
diff --git a/app/assets/javascripts/filtered_search/recent_searches_root.js b/app/assets/javascripts/filtered_search/recent_searches_root.js
index c99ed63c4af..f9338b82acf 100644
--- a/app/assets/javascripts/filtered_search/recent_searches_root.js
+++ b/app/assets/javascripts/filtered_search/recent_searches_root.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content';
+import RecentSearchesDropdownContent from './components/recent_searches_dropdown_content.vue';
import eventHub from './event_hub';
class RecentSearchesRoot {
@@ -33,7 +33,7 @@ class RecentSearchesRoot {
this.vm = new Vue({
el: this.wrapperElement,
components: {
- 'recent-searches-dropdown-content': RecentSearchesDropdownContent,
+ RecentSearchesDropdownContent,
},
data() { return state; },
template: `
diff --git a/app/assets/javascripts/groups/components/app.vue b/app/assets/javascripts/groups/components/app.vue
index b8f0566f48c..0578f43d5af 100644
--- a/app/assets/javascripts/groups/components/app.vue
+++ b/app/assets/javascripts/groups/components/app.vue
@@ -152,14 +152,14 @@ export default {
showLeaveGroupModal(group, parentGroup) {
this.targetGroup = group;
this.targetParentGroup = parentGroup;
- this.updateModal = true;
+ this.showModal = true;
this.groupLeaveConfirmationMessage = s__(`GroupsTree|Are you sure you want to leave the "${group.fullName}" group?`);
},
hideLeaveGroupModal() {
- this.updateModal = false;
+ this.showModal = false;
},
leaveGroup() {
- this.updateModal = false;
+ this.showModal = false;
this.targetGroup.isBeingRemoved = true;
this.service.leaveGroup(this.targetGroup.leavePath)
.then(res => res.json())
@@ -208,9 +208,9 @@ export default {
:page-info="pageInfo"
/>
<modal
- v-show="showModal"
- :primary-button-label="__('Leave')"
+ v-if="showModal"
kind="warning"
+ :primary-button-label="__('Leave')"
:title="__('Are you sure?')"
:text="groupLeaveConfirmationMessage"
@cancel="hideLeaveGroupModal"
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list.vue b/app/assets/javascripts/ide/components/commit_sidebar/list.vue
deleted file mode 100644
index a8459b011df..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/list.vue
+++ /dev/null
@@ -1,65 +0,0 @@
-<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/ide/components/commit_sidebar/list_collapsed.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
deleted file mode 100644
index 6a0262f271b..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_collapsed.vue
+++ /dev/null
@@ -1,35 +0,0 @@
-<script>
- import { mapGetters } from 'vuex';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- },
- computed: {
- ...mapGetters([
- 'addedFiles',
- 'modifiedFiles',
- ]),
- },
- };
-</script>
-
-<template>
- <div
- class="multi-file-commit-list-collapsed text-center"
- >
- <icon
- name="file-addition"
- :size="18"
- css-classes="multi-file-addition append-bottom-10"
- />
- {{ addedFiles.length }}
- <icon
- name="file-modified"
- :size="18"
- css-classes="multi-file-modified prepend-top-10 append-bottom-10"
- />
- {{ modifiedFiles.length }}
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue b/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
deleted file mode 100644
index 742f746e02f..00000000000
--- a/app/assets/javascripts/ide/components/commit_sidebar/list_item.vue
+++ /dev/null
@@ -1,36 +0,0 @@
-<script>
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- },
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- iconName() {
- return this.file.tempFile ? 'file-addition' : 'file-modified';
- },
- iconClass() {
- return `multi-file-${this.file.tempFile ? 'addition' : 'modified'} append-right-8`;
- },
- },
- };
-</script>
-
-<template>
- <div class="multi-file-commit-list-item">
- <icon
- :name="iconName"
- :size="16"
- :css-classes="iconClass"
- />
- <span class="multi-file-commit-list-path">
- {{ file.path }}
- </span>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide.vue b/app/assets/javascripts/ide/components/ide.vue
deleted file mode 100644
index 89981ab2c65..00000000000
--- a/app/assets/javascripts/ide/components/ide.vue
+++ /dev/null
@@ -1,99 +0,0 @@
-<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 {
- components: {
- ideSidebar,
- ideContextbar,
- repoTabs,
- repoFileButtons,
- ideStatusBar,
- repoEditor,
- repoPreview,
- },
- props: {
- emptyStateSvgPath: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'currentBlobView',
- 'selectedFile',
- ]),
- ...mapGetters([
- 'changedFiles',
- 'activeFile',
- ]),
- },
- 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">
- <div class="row js-empty-state">
- <div class="col-xs-12">
- <div class="svg-content svg-250">
- <img :src="emptyStateSvgPath" />
- </div>
- </div>
- <div class="col-xs-12">
- <div class="text-content text-center">
- <h4>
- Welcome to the GitLab IDE
- </h4>
- <p>
- You can select a file in the left sidebar to begin
- editing and use the right sidebar to commit your changes.
- </p>
- </div>
- </div>
- </div>
- </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
deleted file mode 100644
index dd947f66969..00000000000
--- a/app/assets/javascripts/ide/components/ide_context_bar.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
- import { mapGetters, mapState, mapActions } from 'vuex';
- import repoCommitSection from './repo_commit_section.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import panelResizer from '../../vue_shared/components/panel_resizer.vue';
-
- export default {
- components: {
- repoCommitSection,
- icon,
- panelResizer,
- },
- data() {
- return {
- width: 290,
- };
- },
- computed: {
- ...mapState([
- 'rightPanelCollapsed',
- ]),
- ...mapGetters([
- 'changedFiles',
- ]),
- currentIcon() {
- return this.rightPanelCollapsed ? 'angle-double-left' : 'angle-double-right';
- },
- maxSize() {
- return window.innerWidth / 2;
- },
- panelStyle() {
- if (!this.rightPanelCollapsed) {
- return { width: `${this.width}px` };
- }
- return {};
- },
- },
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
- resizingStarted() {
- this.setResizingStatus(true);
- },
- resizingEnded() {
- this.setResizingStatus(false);
- },
- },
- };
-</script>
-
-<template>
- <div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': rightPanelCollapsed,
- }"
- :style="panelStyle"
- >
- <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 />
- </div>
- <panel-resizer
- :size.sync="width"
- :enabled="!rightPanelCollapsed"
- :start-size="290"
- :min-size="200"
- :max-size="maxSize"
- @resize-start="resizingStarted"
- @resize-end="resizingEnded"
- side="left"
- />
- </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
deleted file mode 100644
index af2f7341a91..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_branches_tree.vue
+++ /dev/null
@@ -1,47 +0,0 @@
-<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"
- />
- {{ branch.name }}
- </div>
- <div class="branch-header-btns">
- <new-dropdown
- :project-id="projectId"
- :branch="branch.name"
- path=""
- />
- </div>
- </div>
- <div>
- <repo-tree :tree-id="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
deleted file mode 100644
index ed49a0e72a2..00000000000
--- a/app/assets/javascripts/ide/components/ide_project_tree.vue
+++ /dev/null
@@ -1,49 +0,0 @@
-<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 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
deleted file mode 100644
index 4651e345d75..00000000000
--- a/app/assets/javascripts/ide/components/ide_repo_tree.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
-import { mapState } from 'vuex';
-import repoPreviousDirectory from './repo_prev_directory.vue';
-import repoFile from './repo_file.vue';
-import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-import { treeList } from '../stores/utils';
-
-export default {
- components: {
- repoPreviousDirectory,
- repoFile,
- skeletonLoadingContainer,
- },
- props: {
- treeId: {
- type: String,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'trees',
- 'isRoot',
- ]),
- ...mapState({
- projectName(state) {
- return state.project.name;
- },
- }),
- fetchedList() {
- return treeList(this.$store.state, this.treeId);
- },
- hasPreviousDirectory() {
- return !this.isRoot && this.fetchedList.length;
- },
- showLoading() {
- if (this.trees[this.treeId]) {
- return this.trees[this.treeId].loading;
- }
- return true;
- },
- },
-};
-</script>
-
-<template>
- <div>
- <div class="ide-file-list">
- <table class="table">
- <tbody
- v-if="treeId"
- >
- <repo-previous-directory
- v-if="hasPreviousDirectory"
- />
- <template v-if="showLoading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <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
deleted file mode 100644
index a68f8ce0169..00000000000
--- a/app/assets/javascripts/ide/components/ide_side_bar.vue
+++ /dev/null
@@ -1,114 +0,0 @@
-<script>
- import { mapState, mapActions } from 'vuex';
- import projectTree from './ide_project_tree.vue';
- import icon from '../../vue_shared/components/icon.vue';
- import panelResizer from '../../vue_shared/components/panel_resizer.vue';
- import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-
- export default {
- components: {
- projectTree,
- icon,
- panelResizer,
- skeletonLoadingContainer,
- },
- data() {
- return {
- width: 290,
- };
- },
- computed: {
- ...mapState([
- 'loading',
- 'projects',
- 'leftPanelCollapsed',
- ]),
- currentIcon() {
- return this.leftPanelCollapsed ? 'angle-double-right' : 'angle-double-left';
- },
- maxSize() {
- return window.innerWidth / 2;
- },
- panelStyle() {
- if (!this.leftPanelCollapsed) {
- return { width: `${this.width}px` };
- }
- return {};
- },
- showLoading() {
- return this.loading;
- },
- },
- methods: {
- ...mapActions([
- 'setPanelCollapsedStatus',
- 'setResizingStatus',
- ]),
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'left',
- collapsed: !this.leftPanelCollapsed,
- });
- },
- resizingStarted() {
- this.setResizingStatus(true);
- },
- resizingEnded() {
- this.setResizingStatus(false);
- },
- },
- };
-</script>
-
-<template>
- <div
- class="multi-file-commit-panel"
- :class="{
- 'is-collapsed': leftPanelCollapsed,
- }"
- :style="panelStyle"
- >
- <div class="multi-file-commit-panel-inner">
- <template v-if="showLoading">
- <div
- class="multi-file-loading-container"
- v-for="n in 3"
- :key="n"
- >
- <skeleton-loading-container />
- </div>
- </template>
- <project-tree
- v-for="project 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>
- <panel-resizer
- :size.sync="width"
- :enabled="!leftPanelCollapsed"
- :start-size="290"
- :min-size="200"
- :max-size="maxSize"
- @resize-start="resizingStarted"
- @resize-end="resizingEnded"
- side="right"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/ide_status_bar.vue b/app/assets/javascripts/ide/components/ide_status_bar.vue
deleted file mode 100644
index e48c446c4a4..00000000000
--- a/app/assets/javascripts/ide/components/ide_status_bar.vue
+++ /dev/null
@@ -1,66 +0,0 @@
-<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 {
- components: {
- icon,
- },
- directives: {
- tooltip,
- },
- mixins: [
- timeAgoMixin,
- ],
- props: {
- file: {
- type: Object,
- required: true,
- },
- },
- computed: {
- ...mapState([
- 'selectedFile',
- ]),
- },
- };
-</script>
-
-<template>
- <div class="ide-status-bar">
- <div>
- <icon
- name="branch"
- :size="12"
- />
- {{ 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/ide/components/new_branch_form.vue b/app/assets/javascripts/ide/components/new_branch_form.vue
deleted file mode 100644
index 56e31256132..00000000000
--- a/app/assets/javascripts/ide/components/new_branch_form.vue
+++ /dev/null
@@ -1,108 +0,0 @@
-<script>
- import { mapState, mapActions } from 'vuex';
- import flash, { hideFlash } from '../../flash';
- import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-
- export default {
- components: {
- loadingIcon,
- },
- data() {
- return {
- branchName: '',
- loading: false,
- };
- },
- computed: {
- ...mapState([
- 'currentBranch',
- ]),
- btnDisabled() {
- return this.loading || this.branchName === '';
- },
- },
- created() {
- // Dropdown is outside of Vue instance & is controlled by Bootstrap
- this.$dropdown = $('.git-revision-dropdown');
-
- // text element is outside Vue app
- this.dropdownText = document.querySelector('.project-refs-form .dropdown-toggle-text');
- },
- methods: {
- ...mapActions([
- 'createNewBranch',
- ]),
- toggleDropdown() {
- this.$dropdown.dropdown('toggle');
- },
- submitNewBranch() {
- // need to query as the element is appended outside of Vue
- const flashEl = this.$refs.flashContainer.querySelector('.flash-alert');
-
- this.loading = true;
-
- if (flashEl) {
- hideFlash(flashEl, false);
- }
-
- this.createNewBranch(this.branchName)
- .then(() => {
- this.loading = false;
- this.branchName = '';
-
- if (this.dropdownText) {
- this.dropdownText.textContent = this.currentBranchId;
- }
-
- this.toggleDropdown();
- })
- .catch(res => res.json().then((data) => {
- this.loading = false;
- flash(data.message, 'alert', this.$el);
- }));
- },
- },
- };
-</script>
-
-<template>
- <div>
- <div
- class="flash-container"
- ref="flashContainer"
- >
- </div>
- <p>
- Create from:
- <code>{{ currentBranch }}</code>
- </p>
- <input
- class="form-control js-new-branch-name"
- type="text"
- placeholder="Name new branch"
- v-model="branchName"
- @keyup.enter.stop.prevent="submitNewBranch"
- />
- <div class="prepend-top-default clearfix">
- <button
- type="button"
- class="btn btn-primary pull-left"
- :disabled="btnDisabled"
- @click.stop.prevent="submitNewBranch"
- >
- <loading-icon
- v-if="loading"
- :inline="true"
- />
- <span>Create</span>
- </button>
- <button
- type="button"
- class="btn btn-default pull-right"
- @click.stop.prevent="toggleDropdown"
- >
- Cancel
- </button>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/index.vue b/app/assets/javascripts/ide/components/new_dropdown/index.vue
deleted file mode 100644
index ef653357f5f..00000000000
--- a/app/assets/javascripts/ide/components/new_dropdown/index.vue
+++ /dev/null
@@ -1,101 +0,0 @@
-<script>
- import newModal from './modal.vue';
- import upload from './upload.vue';
- import icon from '../../../vue_shared/components/icon.vue';
-
- export default {
- components: {
- icon,
- newModal,
- upload,
- },
- props: {
- branch: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
- parent: {
- type: Object,
- default: null,
- },
- },
- data() {
- return {
- openModal: false,
- modalType: '',
- };
- },
- methods: {
- createNewItem(type) {
- this.modalType = type;
- this.openModal = true;
- },
- hideModal() {
- this.openModal = false;
- },
- },
- };
-</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"
- @hide="hideModal"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/modal.vue b/app/assets/javascripts/ide/components/new_dropdown/modal.vue
deleted file mode 100644
index 36cd825c6dd..00000000000
--- a/app/assets/javascripts/ide/components/new_dropdown/modal.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<script>
- import { mapActions, mapState } from 'vuex';
- import { __ } from '../../../locale';
- import modal from '../../../vue_shared/components/modal.vue';
-
- export default {
- components: {
- modal,
- },
- props: {
- branchId: {
- type: String,
- required: true,
- },
- parent: {
- type: Object,
- default: null,
- },
- type: {
- type: String,
- required: true,
- },
- path: {
- type: String,
- required: true,
- },
- },
- data() {
- return {
- entryName: this.path !== '' ? `${this.path}/` : '',
- };
- },
- computed: {
- ...mapState([
- 'currentProjectId',
- ]),
- modalTitle() {
- if (this.type === 'tree') {
- return __('Create new directory');
- }
-
- return __('Create new file');
- },
- buttonLabel() {
- if (this.type === 'tree') {
- return __('Create directory');
- }
-
- return __('Create file');
- },
- formLabelName() {
- if (this.type === 'tree') {
- return __('Directory name');
- }
-
- return __('File name');
- },
- },
- mounted() {
- this.$refs.fieldName.focus();
- },
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createEntryInStore() {
- this.createTempEntry({
- projectId: this.currentProjectId,
- branchId: this.branchId,
- parent: this.parent,
- name: this.entryName.replace(new RegExp(`^${this.path}/`), ''),
- type: this.type,
- });
-
- this.hideModal();
- },
- hideModal() {
- this.$emit('hide');
- },
- },
- };
-</script>
-
-<template>
- <modal
- :title="modalTitle"
- :primary-button-label="buttonLabel"
- kind="success"
- @cancel="hideModal"
- @submit="createEntryInStore"
- >
- <form
- class="form-horizontal"
- slot="body"
- @submit.prevent="createEntryInStore"
- >
- <fieldset class="form-group append-bottom-0">
- <label class="label-light col-sm-3">
- {{ formLabelName }}
- </label>
- <div class="col-sm-9">
- <input
- type="text"
- class="form-control"
- v-model="entryName"
- ref="fieldName"
- />
- </div>
- </fieldset>
- </form>
- </modal>
-</template>
diff --git a/app/assets/javascripts/ide/components/new_dropdown/upload.vue b/app/assets/javascripts/ide/components/new_dropdown/upload.vue
deleted file mode 100644
index 6244737fa43..00000000000
--- a/app/assets/javascripts/ide/components/new_dropdown/upload.vue
+++ /dev/null
@@ -1,87 +0,0 @@
-<script>
- import { mapActions, mapState } from 'vuex';
-
- export default {
- props: {
- branchId: {
- type: String,
- required: true,
- },
- parent: {
- type: Object,
- default: null,
- },
- },
- computed: {
- ...mapState([
- 'trees',
- 'currentProjectId',
- ]),
- },
- mounted() {
- this.$refs.fileUpload.addEventListener('change', this.openFile);
- },
- beforeDestroy() {
- this.$refs.fileUpload.removeEventListener('change', this.openFile);
- },
- methods: {
- ...mapActions([
- 'createTempEntry',
- ]),
- createFile(target, file, isText) {
- const { name } = file;
- let { result } = target;
-
- if (!isText) {
- result = result.split('base64,')[1];
- }
-
- this.createTempEntry({
- name,
- projectId: this.currentProjectId,
- branchId: this.branchId,
- parent: this.parent,
- type: 'blob',
- content: result,
- base64: !isText,
- });
- },
- readFile(file) {
- const reader = new FileReader();
- const isText = file.type.match(/text.*/) !== null;
-
- reader.addEventListener('load', e => this.createFile(e.target, file, isText), { once: true });
-
- if (isText) {
- reader.readAsText(file);
- } else {
- reader.readAsDataURL(file);
- }
- },
- openFile() {
- Array.from(this.$refs.fileUpload.files).forEach(file => this.readFile(file));
- },
- startFileUpload() {
- this.$refs.fileUpload.click();
- },
- },
- };
-</script>
-
-<template>
- <div>
- <a
- href="#"
- role="button"
- @click.prevent="startFileUpload"
- >
- {{ __('Upload file') }}
- </a>
- <input
- id="file-upload"
- type="file"
- class="hidden"
- ref="fileUpload"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_commit_section.vue b/app/assets/javascripts/ide/components/repo_commit_section.vue
deleted file mode 100644
index 96b1bb78c1d..00000000000
--- a/app/assets/javascripts/ide/components/repo_commit_section.vue
+++ /dev/null
@@ -1,171 +0,0 @@
-<script>
-import { mapGetters, mapState, mapActions } from 'vuex';
-import tooltip from '../../vue_shared/directives/tooltip';
-import icon from '../../vue_shared/components/icon.vue';
-import modal from '../../vue_shared/components/modal.vue';
-import commitFilesList from './commit_sidebar/list.vue';
-
-export default {
- components: {
- modal,
- icon,
- commitFilesList,
- },
- directives: {
- tooltip,
- },
- data() {
- return {
- showNewBranchModal: false,
- submitCommitsLoading: false,
- startNewMR: false,
- commitMessage: '',
- };
- },
- computed: {
- ...mapState([
- 'currentProjectId',
- 'currentBranchId',
- 'rightPanelCollapsed',
- ]),
- ...mapGetters([
- 'changedFiles',
- ]),
- commitButtonDisabled() {
- return this.commitMessage === '' || this.submitCommitsLoading || !this.changedFiles.length;
- },
- commitMessageCount() {
- return this.commitMessage.length;
- },
- },
- methods: {
- ...mapActions([
- 'checkCommitStatus',
- 'commitChanges',
- 'getTreeData',
- 'setPanelCollapsedStatus',
- ]),
- makeCommit(newBranch = false) {
- const createNewBranch = newBranch || this.startNewMR;
-
- const payload = {
- branch: createNewBranch ?
- `${this.currentBranchId}-${new Date().getTime().toString()}` :
- this.currentBranchId,
- commit_message: this.commitMessage,
- actions: this.changedFiles.map(f => ({
- action: f.tempFile ? 'create' : 'update',
- file_path: f.path,
- content: f.content,
- encoding: f.base64 ? 'base64' : 'text',
- })),
- start_branch: createNewBranch ? this.currentBranchId : undefined,
- };
-
- this.showNewBranchModal = false;
- this.submitCommitsLoading = true;
-
- this.commitChanges({ payload, newMr: this.startNewMR })
- .then(() => {
- this.submitCommitsLoading = false;
- this.commitMessage = '';
- this.startNewMR = false;
- })
- .catch(() => {
- this.submitCommitsLoading = false;
- });
- },
- tryCommit() {
- this.submitCommitsLoading = true;
-
- this.checkCommitStatus()
- .then((branchChanged) => {
- if (branchChanged) {
- this.showNewBranchModal = true;
- } else {
- this.makeCommit();
- }
- })
- .catch(() => {
- this.submitCommitsLoading = false;
- });
- },
- toggleCollapsed() {
- this.setPanelCollapsedStatus({
- side: 'right',
- collapsed: !this.rightPanelCollapsed,
- });
- },
- },
-};
-</script>
-
-<template>
- <div class="multi-file-commit-panel-section">
- <modal
- v-if="showNewBranchModal"
- :primary-button-label="__('Create new branch')"
- kind="primary"
- :title="__('Branch has changed')"
- :text="__(`This branch has changed since
-you started editing. Would you like to create a new branch?`)"
- @cancel="showNewBranchModal = false"
- @submit="makeCommit(true)"
- />
- <commit-files-list
- title="Staged"
- :file-list="changedFiles"
- :collapsed="rightPanelCollapsed"
- @toggleCollapsed="toggleCollapsed"
- />
- <form
- class="form-horizontal multi-file-commit-form"
- @submit.prevent="tryCommit"
- v-if="!rightPanelCollapsed"
- >
- <div class="multi-file-commit-fieldset">
- <textarea
- class="form-control multi-file-commit-message"
- name="commit-message"
- v-model="commitMessage"
- placeholder="Commit message"
- >
- </textarea>
- </div>
- <div class="multi-file-commit-fieldset">
- <label
- v-tooltip
- title="Create a new merge request with these changes"
- data-container="body"
- data-placement="top"
- >
- <input
- type="checkbox"
- v-model="startNewMR"
- />
- Merge Request
- </label>
- <button
- type="submit"
- :disabled="commitButtonDisabled"
- class="btn btn-default btn-sm append-right-10 prepend-left-10"
- :class="{ disabled: submitCommitsLoading }"
- >
- <i
- v-if="submitCommitsLoading"
- class="js-commit-loading-icon fa fa-spinner fa-spin"
- aria-hidden="true"
- aria-label="loading"
- >
- </i>
- Commit
- </button>
- <div
- class="multi-file-commit-message-count"
- >
- {{ commitMessageCount }}
- </div>
- </div>
- </form>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_edit_button.vue b/app/assets/javascripts/ide/components/repo_edit_button.vue
deleted file mode 100644
index c43e9163340..00000000000
--- a/app/assets/javascripts/ide/components/repo_edit_button.vue
+++ /dev/null
@@ -1,57 +0,0 @@
-<script>
-import { mapGetters, mapActions, mapState } from 'vuex';
-import modal from '../../vue_shared/components/modal.vue';
-
-export default {
- components: {
- modal,
- },
- computed: {
- ...mapState([
- 'editMode',
- 'discardPopupOpen',
- ]),
- ...mapGetters([
- 'canEditFile',
- ]),
- buttonLabel() {
- return this.editMode ? this.__('Cancel edit') : this.__('Edit');
- },
- },
- methods: {
- ...mapActions([
- 'toggleEditMode',
- 'closeDiscardPopup',
- ]),
- },
-};
-</script>
-
-<template>
- <div class="editable-mode">
- <button
- v-if="canEditFile"
- class="btn btn-default"
- type="button"
- @click.prevent="toggleEditMode()">
- <i
- v-if="!editMode"
- class="fa fa-pencil"
- aria-hidden="true">
- </i>
- <span>
- {{ buttonLabel }}
- </span>
- </button>
- <modal
- v-if="discardPopupOpen"
- class="text-left"
- :primary-button-label="__('Discard changes')"
- kind="warning"
- :title="__('Are you sure?')"
- :text="__('Are you sure you want to discard your changes?')"
- @cancel="closeDiscardPopup"
- @submit="toggleEditMode(true)"
- />
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
deleted file mode 100644
index f99228012f4..00000000000
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ /dev/null
@@ -1,136 +0,0 @@
-<script>
-/* global monaco */
-import { mapState, mapGetters, mapActions } from 'vuex';
-import flash from '../../flash';
-import monacoLoader from '../monaco_loader';
-import Editor from '../lib/editor';
-
-export default {
- computed: {
- ...mapGetters([
- 'activeFile',
- 'activeFileExtension',
- ]),
- ...mapState([
- 'leftPanelCollapsed',
- 'rightPanelCollapsed',
- 'panelResizing',
- ]),
- shouldHideEditor() {
- return this.activeFile.binary && !this.activeFile.raw;
- },
- },
- watch: {
- activeFile(oldVal, newVal) {
- if (newVal && !newVal.active) {
- this.initMonaco();
- }
- },
- leftPanelCollapsed() {
- this.editor.updateDimensions();
- },
- rightPanelCollapsed() {
- this.editor.updateDimensions();
- },
- panelResizing(isResizing) {
- if (isResizing === false) {
- this.editor.updateDimensions();
- }
- },
- },
- beforeDestroy() {
- this.editor.dispose();
- },
- mounted() {
- if (this.editor && monaco) {
- this.initMonaco();
- } else {
- monacoLoader(['vs/editor/editor.main'], () => {
- this.editor = Editor.create(monaco);
-
- this.initMonaco();
- });
- }
- },
- methods: {
- ...mapActions([
- 'getRawFileData',
- 'changeFileContent',
- 'setFileLanguage',
- 'setEditorPosition',
- 'setFileEOL',
- ]),
- initMonaco() {
- if (this.shouldHideEditor) return;
-
- this.editor.clearEditor();
-
- this.getRawFileData(this.activeFile)
- .then(() => {
- this.editor.createInstance(this.$refs.editor);
- })
- .then(() => this.setupEditor())
- .catch((err) => {
- flash('Error setting up monaco. Please try again.', 'alert', document, null, false, true);
- throw err;
- });
- },
- setupEditor() {
- if (!this.activeFile) return;
-
- 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,
- });
- },
- },
-};
-</script>
-
-<template>
- <div
- id="ide"
- class="blob-viewer-container blob-editor-container"
- >
- <div
- v-if="shouldHideEditor"
- v-html="activeFile.html"
- >
- </div>
- <div
- v-show="!shouldHideEditor"
- ref="editor"
- class="multi-file-editor-holder"
- >
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_file.vue b/app/assets/javascripts/ide/components/repo_file.vue
deleted file mode 100644
index 110918872fb..00000000000
--- a/app/assets/javascripts/ide/components/repo_file.vue
+++ /dev/null
@@ -1,165 +0,0 @@
-<script>
- 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';
- import fileIcon from '../../vue_shared/components/file_icon.vue';
-
- export default {
- components: {
- skeletonLoadingContainer,
- newDropdown,
- fileIcon,
- },
- mixins: [
- timeAgoMixin,
- ],
- props: {
- file: {
- type: Object,
- required: true,
- },
- showExtraColumns: {
- type: Boolean,
- default: false,
- },
- },
- computed: {
- ...mapState([
- 'leftPanelCollapsed',
- ]),
- isSubmodule() {
- return this.file.type === 'submodule';
- },
- isTree() {
- return this.file.type === 'tree';
- },
- levelIndentation() {
- if (this.file.level > 0) {
- return {
- marginLeft: `${this.file.level * 16}px`,
- };
- }
- return {};
- },
- shortId() {
- return this.file.id.substr(0, 8);
- },
- submoduleColSpan() {
- 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,
- };
- },
- },
- updated() {
- if (this.file.type === 'blob' && this.file.active) {
- this.$el.scrollIntoView();
- }
- },
- methods: {
- 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}`);
- },
- },
- };
-</script>
-
-<template>
- <tr
- class="file"
- :class="fileClass"
- @click="clickFile(file)">
- <td
- class="multi-file-table-name"
- :colspan="submoduleColSpan"
- >
- <a
- class="repo-file-name"
- >
- <file-icon
- :file-name="file.name"
- :loading="file.loading"
- :folder="file.type === 'tree'"
- :opened="file.opened"
- :style="levelIndentation"
- :size="16"
- />
- {{ 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="file.changed || file.tempFile"
- :class="changedClass"
- aria-hidden="true"
- >
- </i>
- <template v-if="isSubmodule && file.id">
- @
- <span class="commit-sha">
- <a
- @click.stop
- :href="file.tree_url"
- >
- {{ shortId }}
- </a>
- </span>
- </template>
- </td>
-
- <template v-if="showExtraColumns && !isSubmodule">
- <td class="multi-file-table-col-commit-message hidden-sm hidden-xs">
- <a
- v-if="file.lastCommit.message"
- @click.stop
- :href="file.lastCommit.url"
- >
- {{ file.lastCommit.message }}
- </a>
- <skeleton-loading-container
- v-else
- :small="true"
- />
- </td>
-
- <td class="commit-update hidden-xs text-right">
- <span
- v-if="file.lastCommit.updatedAt"
- :title="tooltipTitle(file.lastCommit.updatedAt)"
- >
- {{ timeFormated(file.lastCommit.updatedAt) }}
- </span>
- <skeleton-loading-container
- v-else
- class="animation-container-right"
- :small="true"
- />
- </td>
- </template>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_file_buttons.vue b/app/assets/javascripts/ide/components/repo_file_buttons.vue
deleted file mode 100644
index aabc0d8eada..00000000000
--- a/app/assets/javascripts/ide/components/repo_file_buttons.vue
+++ /dev/null
@@ -1,60 +0,0 @@
-<script>
-import { mapGetters } from 'vuex';
-
-export default {
- computed: {
- ...mapGetters([
- 'activeFile',
- ]),
- showButtons() {
- return this.activeFile.rawPath ||
- this.activeFile.blamePath ||
- this.activeFile.commitsPath ||
- this.activeFile.permalink;
- },
- rawDownloadButtonLabel() {
- return this.activeFile.binary ? 'Download' : 'Raw';
- },
- },
-};
-</script>
-
-<template>
- <div
- v-if="showButtons"
- class="multi-file-editor-btn-group"
- >
- <a
- :href="activeFile.rawPath"
- target="_blank"
- class="btn btn-default btn-sm raw"
- rel="noopener noreferrer">
- {{ rawDownloadButtonLabel }}
- </a>
-
- <div
- class="btn-group"
- role="group"
- aria-label="File actions"
- >
- <a
- :href="activeFile.blamePath"
- class="btn btn-default btn-sm blame"
- >
- Blame
- </a>
- <a
- :href="activeFile.commitsPath"
- class="btn btn-default btn-sm history"
- >
- History
- </a>
- <a
- :href="activeFile.permalink"
- class="btn btn-default btn-sm permalink"
- >
- Permalink
- </a>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_loading_file.vue b/app/assets/javascripts/ide/components/repo_loading_file.vue
deleted file mode 100644
index 3aeb6f0b28f..00000000000
--- a/app/assets/javascripts/ide/components/repo_loading_file.vue
+++ /dev/null
@@ -1,42 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import skeletonLoadingContainer from '../../vue_shared/components/skeleton_loading_container.vue';
-
- export default {
- components: {
- skeletonLoadingContainer,
- },
- computed: {
- ...mapState([
- 'leftPanelCollapsed',
- ]),
- },
- };
-</script>
-
-<template>
- <tr
- class="loading-file"
- aria-label="Loading files"
- >
- <td class="multi-file-table-col-name">
- <skeleton-loading-container
- :small="true"
- />
- </td>
- <template v-if="!leftPanelCollapsed">
- <td class="hidden-sm hidden-xs">
- <skeleton-loading-container
- :small="true"
- />
- </td>
-
- <td class="hidden-xs">
- <skeleton-loading-container
- class="animation-container-right"
- :small="true"
- />
- </td>
- </template>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_prev_directory.vue b/app/assets/javascripts/ide/components/repo_prev_directory.vue
deleted file mode 100644
index 7cd359ea4ed..00000000000
--- a/app/assets/javascripts/ide/components/repo_prev_directory.vue
+++ /dev/null
@@ -1,32 +0,0 @@
-<script>
- import { mapState, mapActions } from 'vuex';
-
- export default {
- computed: {
- ...mapState([
- 'parentTreeUrl',
- 'leftPanelCollapsed',
- ]),
- colSpanCondition() {
- return this.leftPanelCollapsed ? undefined : 3;
- },
- },
- methods: {
- ...mapActions([
- 'getTreeData',
- ]),
- },
- };
-</script>
-
-<template>
- <tr class="file prev-directory">
- <td
- :colspan="colSpanCondition"
- class="table-cell"
- @click.prevent="getTreeData({ endpoint: parentTreeUrl })"
- >
- <a :href="parentTreeUrl">...</a>
- </td>
- </tr>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_preview.vue b/app/assets/javascripts/ide/components/repo_preview.vue
deleted file mode 100644
index e47270a9855..00000000000
--- a/app/assets/javascripts/ide/components/repo_preview.vue
+++ /dev/null
@@ -1,71 +0,0 @@
-<script>
- import { mapGetters } from 'vuex';
- import LineHighlighter from '../../line_highlighter';
- import syntaxHighlight from '../../syntax_highlight';
-
- export default {
- computed: {
- ...mapGetters([
- 'activeFile',
- ]),
- renderErrorTooLarge() {
- return this.activeFile.renderError === 'too_large';
- },
- },
- mounted() {
- this.highlightFile();
- this.lineHighlighter = new LineHighlighter({
- fileHolderSelector: '.blob-viewer-container',
- scrollFileHolder: true,
- });
- },
- updated() {
- this.$nextTick(() => {
- this.highlightFile();
- });
- },
- methods: {
- highlightFile() {
- syntaxHighlight($(this.$el).find('.file-content'));
- },
- },
- };
-</script>
-
-<template>
- <div>
- <div
- v-if="!activeFile.renderError"
- v-html="activeFile.html"
- class="multi-file-preview-holder"
- >
- </div>
- <div
- v-else-if="activeFile.tempFile"
- class="vertical-center render-error">
- <p class="text-center">
- The source could not be displayed for this temporary file.
- </p>
- </div>
- <div
- v-else-if="renderErrorTooLarge"
- class="vertical-center render-error">
- <p class="text-center">
- The source could not be displayed because it is too large.
- You can <a
- :href="activeFile.rawPath"
- download>download</a> it instead.
- </p>
- </div>
- <div
- v-else
- class="vertical-center render-error">
- <p class="text-center">
- The source could not be displayed because a rendering error occurred.
- You can <a
- :href="activeFile.rawPath"
- download>download</a> it instead.
- </p>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_tab.vue b/app/assets/javascripts/ide/components/repo_tab.vue
deleted file mode 100644
index 5ed7bddf6ae..00000000000
--- a/app/assets/javascripts/ide/components/repo_tab.vue
+++ /dev/null
@@ -1,74 +0,0 @@
-<script>
- import { mapActions } from 'vuex';
- import fileIcon from '../../vue_shared/components/file_icon.vue';
-
- export default {
- components: {
- fileIcon,
- },
- props: {
- tab: {
- type: Object,
- required: true,
- },
- },
- computed: {
- closeLabel() {
- if (this.tab.changed || this.tab.tempFile) {
- return `${this.tab.name} changed`;
- }
- return `Close ${this.tab.name}`;
- },
- changedClass() {
- const tabChangedObj = {
- 'fa-times close-icon': !this.tab.changed && !this.tab.tempFile,
- 'fa-circle unsaved-icon': this.tab.changed || this.tab.tempFile,
- };
- return tabChangedObj;
- },
- },
-
- methods: {
- ...mapActions([
- 'closeFile',
- ]),
- clickFile(tab) {
- this.$router.push(`/project${tab.url}`);
- },
- },
- };
-</script>
-
-<template>
- <li @click="clickFile(tab)">
- <button
- type="button"
- class="multi-file-tab-close"
- @click.stop.prevent="closeFile({ file: tab })"
- :aria-label="closeLabel"
- :class="{
- 'modified': tab.changed,
- }"
- :disabled="tab.changed"
- >
- <i
- class="fa"
- :class="changedClass"
- aria-hidden="true"
- >
- </i>
- </button>
-
- <div
- class="multi-file-tab"
- :class="{active : tab.active }"
- :title="tab.url"
- >
- <file-icon
- :file-name="tab.name"
- :size="16"
- />
- {{ tab.name }}
- </div>
- </li>
-</template>
diff --git a/app/assets/javascripts/ide/components/repo_tabs.vue b/app/assets/javascripts/ide/components/repo_tabs.vue
deleted file mode 100644
index ca363bba0ef..00000000000
--- a/app/assets/javascripts/ide/components/repo_tabs.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
- import { mapState } from 'vuex';
- import RepoTab from './repo_tab.vue';
-
- export default {
- components: {
- 'repo-tab': RepoTab,
- },
- computed: {
- ...mapState([
- 'openFiles',
- ]),
- },
- };
-</script>
-
-<template>
- <ul
- class="multi-file-tabs list-unstyled append-bottom-0"
- >
- <repo-tab
- v-for="tab in openFiles"
- :key="tab.key"
- :tab="tab"
- />
- </ul>
-</template>
diff --git a/app/assets/javascripts/ide/ide_router.js b/app/assets/javascripts/ide/ide_router.js
deleted file mode 100644
index a7fb9e0588a..00000000000
--- a/app/assets/javascripts/ide/ide_router.js
+++ /dev/null
@@ -1,101 +0,0 @@
-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.', 'alert', document, null, false, true);
- throw e;
- });
- }
- })
- .catch((e) => {
- flash('Error while loading the project data. Please try again.', 'alert', document, null, false, true);
- throw e;
- });
- }
-
- next();
-});
-
-export default router;
diff --git a/app/assets/javascripts/ide/index.js b/app/assets/javascripts/ide/index.js
deleted file mode 100644
index e8a19f47cee..00000000000
--- a/app/assets/javascripts/ide/index.js
+++ /dev/null
@@ -1,31 +0,0 @@
-import Vue from 'vue';
-import ide from './components/ide.vue';
-import store from './stores';
-import router from './ide_router';
-import Translate from '../vue_shared/translate';
-
-function initIde(el) {
- if (!el) return null;
-
- return new Vue({
- el,
- store,
- router,
- components: {
- ide,
- },
- render(createElement) {
- return createElement('ide', {
- props: {
- emptyStateSvgPath: el.dataset.emptyStateSvgPath,
- },
- });
- },
- });
-}
-
-const ideElement = document.getElementById('ide');
-
-Vue.use(Translate);
-
-initIde(ideElement);
diff --git a/app/assets/javascripts/ide/lib/common/disposable.js b/app/assets/javascripts/ide/lib/common/disposable.js
deleted file mode 100644
index 84b29bdb600..00000000000
--- a/app/assets/javascripts/ide/lib/common/disposable.js
+++ /dev/null
@@ -1,14 +0,0 @@
-export default class Disposable {
- constructor() {
- this.disposers = new Set();
- }
-
- add(...disposers) {
- disposers.forEach(disposer => this.disposers.add(disposer));
- }
-
- dispose() {
- this.disposers.forEach(disposer => disposer.dispose());
- this.disposers.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/common/model.js b/app/assets/javascripts/ide/lib/common/model.js
deleted file mode 100644
index 14d9fe4771e..00000000000
--- a/app/assets/javascripts/ide/lib/common/model.js
+++ /dev/null
@@ -1,64 +0,0 @@
-/* global monaco */
-import Disposable from './disposable';
-
-export default class Model {
- constructor(monaco, file) {
- this.monaco = monaco;
- this.disposable = new Disposable();
- this.file = file;
- this.content = file.content !== '' ? file.content : file.raw;
-
- this.disposable.add(
- this.originalModel = this.monaco.editor.createModel(
- this.file.raw,
- undefined,
- new this.monaco.Uri(null, null, `original/${this.file.path}`),
- ),
- this.model = this.monaco.editor.createModel(
- this.content,
- undefined,
- new this.monaco.Uri(null, null, this.file.path),
- ),
- );
-
- this.events = new Map();
- }
-
- get url() {
- 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;
- }
-
- getModel() {
- return this.model;
- }
-
- getOriginalModel() {
- return this.originalModel;
- }
-
- onChange(cb) {
- this.events.set(
- this.path,
- this.disposable.add(
- this.model.onDidChangeContent(e => cb(this.model, e)),
- ),
- );
- }
-
- dispose() {
- this.disposable.dispose();
- this.events.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/common/model_manager.js b/app/assets/javascripts/ide/lib/common/model_manager.js
deleted file mode 100644
index fd462252795..00000000000
--- a/app/assets/javascripts/ide/lib/common/model_manager.js
+++ /dev/null
@@ -1,32 +0,0 @@
-import Disposable from './disposable';
-import Model from './model';
-
-export default class ModelManager {
- constructor(monaco) {
- this.monaco = monaco;
- this.disposable = new Disposable();
- this.models = new Map();
- }
-
- hasCachedModel(path) {
- return this.models.has(path);
- }
-
- addModel(file) {
- if (this.hasCachedModel(file.path)) {
- return this.models.get(file.path);
- }
-
- const model = new Model(this.monaco, file);
- this.models.set(model.path, model);
- this.disposable.add(model);
-
- return model;
- }
-
- dispose() {
- // dispose of all the models
- this.disposable.dispose();
- this.models.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/decorations/controller.js b/app/assets/javascripts/ide/lib/decorations/controller.js
deleted file mode 100644
index 0954b7973c4..00000000000
--- a/app/assets/javascripts/ide/lib/decorations/controller.js
+++ /dev/null
@@ -1,43 +0,0 @@
-export default class DecorationsController {
- constructor(editor) {
- this.editor = editor;
- this.decorations = new Map();
- this.editorDecorations = new Map();
- }
-
- getAllDecorationsForModel(model) {
- if (!this.decorations.has(model.url)) return [];
-
- const modelDecorations = this.decorations.get(model.url);
- const decorations = [];
-
- modelDecorations.forEach(val => decorations.push(...val));
-
- return decorations;
- }
-
- addDecorations(model, decorationsKey, decorations) {
- const decorationMap = this.decorations.get(model.url) || new Map();
-
- decorationMap.set(decorationsKey, decorations);
-
- this.decorations.set(model.url, decorationMap);
-
- this.decorate(model);
- }
-
- decorate(model) {
- const decorations = this.getAllDecorationsForModel(model);
- const oldDecorations = this.editorDecorations.get(model.url) || [];
-
- this.editorDecorations.set(
- model.url,
- this.editor.instance.deltaDecorations(oldDecorations, decorations),
- );
- }
-
- dispose() {
- this.decorations.clear();
- this.editorDecorations.clear();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/diff/controller.js b/app/assets/javascripts/ide/lib/diff/controller.js
deleted file mode 100644
index dc0b1c95e59..00000000000
--- a/app/assets/javascripts/ide/lib/diff/controller.js
+++ /dev/null
@@ -1,71 +0,0 @@
-/* global monaco */
-import { throttle } from 'underscore';
-import DirtyDiffWorker from './diff_worker';
-import Disposable from '../common/disposable';
-
-export const getDiffChangeType = (change) => {
- if (change.modified) {
- return 'modified';
- } else if (change.added) {
- return 'added';
- } else if (change.removed) {
- return 'removed';
- }
-
- return '';
-};
-
-export const getDecorator = change => ({
- range: new monaco.Range(
- change.lineNumber,
- 1,
- change.endLineNumber,
- 1,
- ),
- options: {
- isWholeLine: true,
- linesDecorationsClassName: `dirty-diff dirty-diff-${getDiffChangeType(change)}`,
- },
-});
-
-export default class DirtyDiffController {
- constructor(modelManager, decorationsController) {
- this.disposable = new Disposable();
- this.editorSimpleWorker = null;
- this.modelManager = modelManager;
- this.decorationsController = decorationsController;
- this.dirtyDiffWorker = new DirtyDiffWorker();
- this.throttledComputeDiff = throttle(this.computeDiff, 250);
- this.decorate = this.decorate.bind(this);
-
- this.dirtyDiffWorker.addEventListener('message', this.decorate);
- }
-
- attachModel(model) {
- model.onChange(() => this.throttledComputeDiff(model));
- }
-
- computeDiff(model) {
- this.dirtyDiffWorker.postMessage({
- path: model.path,
- originalContent: model.getOriginalModel().getValue(),
- newContent: model.getModel().getValue(),
- });
- }
-
- reDecorate(model) {
- this.decorationsController.decorate(model);
- }
-
- decorate({ data }) {
- const decorations = data.changes.map(change => getDecorator(change));
- this.decorationsController.addDecorations(data.path, 'dirtyDiff', decorations);
- }
-
- dispose() {
- this.disposable.dispose();
-
- this.dirtyDiffWorker.removeEventListener('message', this.decorate);
- this.dirtyDiffWorker.terminate();
- }
-}
diff --git a/app/assets/javascripts/ide/lib/diff/diff.js b/app/assets/javascripts/ide/lib/diff/diff.js
deleted file mode 100644
index 0e37f5c4704..00000000000
--- a/app/assets/javascripts/ide/lib/diff/diff.js
+++ /dev/null
@@ -1,30 +0,0 @@
-import { diffLines } from 'diff';
-
-// eslint-disable-next-line import/prefer-default-export
-export const computeDiff = (originalContent, newContent) => {
- const changes = diffLines(originalContent, newContent);
-
- let lineNumber = 1;
- return changes.reduce((acc, change) => {
- const findOnLine = acc.find(c => c.lineNumber === lineNumber);
-
- if (findOnLine) {
- Object.assign(findOnLine, change, {
- modified: true,
- endLineNumber: (lineNumber + change.count) - 1,
- });
- } else if ('added' in change || 'removed' in change) {
- acc.push(Object.assign({}, change, {
- lineNumber,
- modified: undefined,
- endLineNumber: (lineNumber + change.count) - 1,
- }));
- }
-
- if (!change.removed) {
- lineNumber += change.count;
- }
-
- return acc;
- }, []);
-};
diff --git a/app/assets/javascripts/ide/lib/diff/diff_worker.js b/app/assets/javascripts/ide/lib/diff/diff_worker.js
deleted file mode 100644
index e74c4046330..00000000000
--- a/app/assets/javascripts/ide/lib/diff/diff_worker.js
+++ /dev/null
@@ -1,10 +0,0 @@
-import { computeDiff } from './diff';
-
-self.addEventListener('message', (e) => {
- const data = e.data;
-
- self.postMessage({
- path: data.path,
- changes: computeDiff(data.originalContent, data.newContent),
- });
-});
diff --git a/app/assets/javascripts/ide/lib/editor.js b/app/assets/javascripts/ide/lib/editor.js
deleted file mode 100644
index 51255f15658..00000000000
--- a/app/assets/javascripts/ide/lib/editor.js
+++ /dev/null
@@ -1,110 +0,0 @@
-import _ from 'underscore';
-import DecorationsController from './decorations/controller';
-import DirtyDiffController from './diff/controller';
-import Disposable from './common/disposable';
-import ModelManager from './common/model_manager';
-import editorOptions from './editor_options';
-
-export default class Editor {
- static create(monaco) {
- this.editorInstance = new Editor(monaco);
-
- return this.editorInstance;
- }
-
- constructor(monaco) {
- this.monaco = monaco;
- this.currentModel = null;
- this.instance = null;
- this.dirtyDiffController = null;
- this.disposable = new Disposable();
-
- this.disposable.add(
- 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) {
- if (!this.instance) {
- this.disposable.add(
- this.instance = this.monaco.editor.create(domElement, {
- model: null,
- readOnly: false,
- contextmenu: true,
- scrollBeyondLastLine: false,
- minimap: {
- enabled: false,
- },
- }),
- this.dirtyDiffController = new DirtyDiffController(
- this.modelManager, this.decorationsController,
- ),
- );
- }
- }
-
- createModel(file) {
- return this.modelManager.addModel(file);
- }
-
- attachModel(model) {
- this.instance.setModel(model.getModel());
- if (this.dirtyDiffController) this.dirtyDiffController.attachModel(model);
-
- this.currentModel = model;
-
- this.instance.updateOptions(editorOptions.reduce((acc, obj) => {
- Object.keys(obj).forEach((key) => {
- Object.assign(acc, {
- [key]: obj[key](model),
- });
- });
- return acc;
- }, {}));
-
- if (this.dirtyDiffController) this.dirtyDiffController.reDecorate(model);
- }
-
- clearEditor() {
- if (this.instance) {
- this.instance.setModel(null);
- }
- }
-
- 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/ide/lib/editor_options.js b/app/assets/javascripts/ide/lib/editor_options.js
deleted file mode 100644
index 701affc466e..00000000000
--- a/app/assets/javascripts/ide/lib/editor_options.js
+++ /dev/null
@@ -1,2 +0,0 @@
-export default [{
-}];
diff --git a/app/assets/javascripts/ide/monaco_loader.js b/app/assets/javascripts/ide/monaco_loader.js
deleted file mode 100644
index 142a220097b..00000000000
--- a/app/assets/javascripts/ide/monaco_loader.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import monacoContext from 'monaco-editor/dev/vs/loader';
-
-monacoContext.require.config({
- paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
- },
-});
-
-// ignore CDN config and use local assets path for service worker which cannot be cross-domain
-const relativeRootPath = (gon && gon.relative_url_root) || '';
-const monacoPath = `${relativeRootPath}/assets/webpack/monaco-editor/vs`;
-window.MonacoEnvironment = { getWorkerUrl: () => `${monacoPath}/base/worker/workerMain.js` };
-
-// eslint-disable-next-line no-underscore-dangle
-window.__monaco_context__ = monacoContext;
-export default monacoContext.require;
diff --git a/app/assets/javascripts/ide/services/index.js b/app/assets/javascripts/ide/services/index.js
deleted file mode 100644
index 1fb24e93f2e..00000000000
--- a/app/assets/javascripts/ide/services/index.js
+++ /dev/null
@@ -1,47 +0,0 @@
-import Vue from 'vue';
-import VueResource from 'vue-resource';
-import Api from '../../api';
-
-Vue.use(VueResource);
-
-export default {
- getTreeData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json' } });
- },
- getFileData(endpoint) {
- return Vue.http.get(endpoint, { params: { format: 'json' } });
- },
- getRawFileData(file) {
- if (file.tempFile) {
- return Promise.resolve(file.content);
- }
-
- if (file.raw) {
- return Promise.resolve(file.raw);
- }
-
- return Vue.http.get(file.rawPath, { params: { format: 'json' } })
- .then(res => res.text());
- },
- 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);
-
- return Vue.http.post(url, payload);
- },
- commit(projectId, payload) {
- return Api.commitMultiple(projectId, payload);
- },
- getTreeLastCommit(endpoint) {
- return Vue.http.get(endpoint, {
- params: {
- format: 'json',
- },
- });
- },
-};
diff --git a/app/assets/javascripts/ide/stores/actions.js b/app/assets/javascripts/ide/stores/actions.js
deleted file mode 100644
index d007d0ae78f..00000000000
--- a/app/assets/javascripts/ide/stores/actions.js
+++ /dev/null
@@ -1,196 +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';
-import { stripHtml } from '../../lib/utils/text_utility';
-
-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 setResizingStatus = ({ commit }, resizing) => {
- commit(types.SET_RESIZING_STATUS, resizing);
-};
-
-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.', 'alert', document, null, false, true));
-
-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, 'alert', document, null, false, true);
- 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,
- },
- };
-
- let commitMsg = `Your changes have been committed. Commit ${data.short_id}`;
- if (data.stats) {
- commitMsg += ` with ${data.stats.additions} additions, ${data.stats.deletions} deletions.`;
- }
-
- flash(
- commitMsg,
- 'notice',
- document,
- null,
- false,
- true);
- window.dispatchEvent(new Event('resize'));
-
- if (newMr) {
- dispatch('discardAllChanges');
- 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');
-
- window.scrollTo(0, 0);
- }
- })
- .catch((err) => {
- let errMsg = 'Error committing changes. Please try again.';
- if (err.response.data && err.response.data.message) {
- errMsg += ` (${stripHtml(err.response.data.message)})`;
- }
- flash(errMsg, 'alert', document, null, false, true);
- window.dispatchEvent(new Event('resize'));
- });
-
-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
deleted file mode 100644
index bc6fd2d4163..00000000000
--- a/app/assets/javascripts/ide/stores/actions/branch.js
+++ /dev/null
@@ -1,43 +0,0 @@
-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.', 'alert', document, null, false, true);
- 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/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
deleted file mode 100644
index 670af2fb89e..00000000000
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ /dev/null
@@ -1,137 +0,0 @@
-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,
- setPageTitle,
- createTemp,
- findIndexOfFile,
-} from '../utils';
-
-export const closeFile = ({ commit, state, dispatch }, { file, force = false }) => {
- if ((file.changed || file.tempFile) && !force) return;
-
- const indexOfClosedFile = findIndexOfFile(state.openFiles, file);
- const fileWasActive = file.active;
-
- commit(types.TOGGLE_FILE_OPEN, file);
- commit(types.SET_FILE_ACTIVE, { file, active: false });
-
- if (state.openFiles.length > 0 && fileWasActive) {
- const nextIndexToOpen = indexOfClosedFile === 0 ? 0 : indexOfClosedFile - 1;
- const nextFileToOpen = state.openFiles[nextIndexToOpen];
-
- dispatch('setFileActive', nextFileToOpen);
- } else if (!state.openFiles.length) {
- router.push(`/project/${file.projectId}/tree/${file.branchId}/`);
- }
-
- dispatch('getLastCommitData');
-};
-
-export const setFileActive = ({ commit, state, getters, dispatch }, file) => {
- const currentActiveFile = getters.activeFile;
-
- if (file.active) return;
-
- if (currentActiveFile) {
- commit(types.SET_FILE_ACTIVE, { file: currentActiveFile, active: false });
- }
-
- commit(types.SET_FILE_ACTIVE, { file, active: true });
- dispatch('scrollToTab');
-
- // 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) => {
- commit(types.TOGGLE_LOADING, file);
-
- service.getFileData(file.url)
- .then((res) => {
- const pageTitle = decodeURI(normalizeHeaders(res.headers)['PAGE-TITLE']);
-
- setPageTitle(pageTitle);
-
- return res.json();
- })
- .then((data) => {
- commit(types.SET_FILE_DATA, { data, file });
- commit(types.TOGGLE_FILE_OPEN, file);
- dispatch('setFileActive', file);
- commit(types.TOGGLE_LOADING, file);
- })
- .catch(() => {
- commit(types.TOGGLE_LOADING, file);
- flash('Error loading file data. Please try again.', 'alert', document, null, false, true);
- });
-};
-
-export const getRawFileData = ({ commit, dispatch }, file) => service.getRawFileData(file)
- .then((raw) => {
- commit(types.SET_FILE_RAW_DATA, { file, raw });
- })
- .catch(() => flash('Error loading file content. Please try again.', 'alert', document, null, false, true));
-
-export const changeFileContent = ({ commit }, { file, content }) => {
- commit(types.UPDATE_FILE_CONTENT, { file, content });
-};
-
-export const setFileLanguage = ({ state, commit }, { fileLanguage }) => {
- if (state.selectedFile) {
- commit(types.SET_FILE_LANGUAGE, { file: state.selectedFile, fileLanguage });
- }
-};
-
-export const setFileEOL = ({ state, commit }, { eol }) => {
- if (state.selectedFile) {
- commit(types.SET_FILE_EOL, { file: state.selectedFile, eol });
- }
-};
-
-export const setEditorPosition = ({ state, commit }, { editorRow, editorColumn }) => {
- if (state.selectedFile) {
- 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({
- projectId,
- branchId,
- name: name.replace(`${path}/`, ''),
- path,
- type: 'blob',
- level: parent.level !== undefined ? parent.level + 1 : 0,
- changed: true,
- content,
- base64,
- url: newUrl,
- });
-
- if (findEntry(parent.tree, 'blob', file.name)) return flash(`The name "${file.name}" is already taken in this directory.`, 'alert', document, null, false, true);
-
- commit(types.CREATE_TMP_FILE, {
- parent,
- file,
- });
- commit(types.TOGGLE_FILE_OPEN, file);
- dispatch('setFileActive', file);
-
- if (!state.editMode && !file.base64) {
- 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
deleted file mode 100644
index faeceb430a2..00000000000
--- a/app/assets/javascripts/ide/stores/actions/project.js
+++ /dev/null
@@ -1,27 +0,0 @@
-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) {
- commit(types.TOGGLE_LOADING, state);
- service.getProjectData(namespace, projectId)
- .then(res => res.data)
- .then((data) => {
- commit(types.TOGGLE_LOADING, state);
- 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.', 'alert', document, null, false, true);
- 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
deleted file mode 100644
index 302ba45edee..00000000000
--- a/app/assets/javascripts/ide/stores/actions/tree.js
+++ /dev/null
@@ -1,188 +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 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.', 'alert', document, null, false, true);
- 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.', 'alert', document, null, false, true));
-};
-
-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
deleted file mode 100644
index 6b51ccff817..00000000000
--- a/app/assets/javascripts/ide/stores/getters.js
+++ /dev/null
@@ -1,19 +0,0 @@
-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/ide/stores/index.js b/app/assets/javascripts/ide/stores/index.js
deleted file mode 100644
index 6ac9bfd8189..00000000000
--- a/app/assets/javascripts/ide/stores/index.js
+++ /dev/null
@@ -1,15 +0,0 @@
-import Vue from 'vue';
-import Vuex from 'vuex';
-import state from './state';
-import * as actions from './actions';
-import * as getters from './getters';
-import mutations from './mutations';
-
-Vue.use(Vuex);
-
-export default new Vuex.Store({
- state: state(),
- actions,
- mutations,
- getters,
-});
diff --git a/app/assets/javascripts/ide/stores/mutation_types.js b/app/assets/javascripts/ide/stores/mutation_types.js
deleted file mode 100644
index 69b218a5e7d..00000000000
--- a/app/assets/javascripts/ide/stores/mutation_types.js
+++ /dev/null
@@ -1,46 +0,0 @@
-export const SET_INITIAL_DATA = 'SET_INITIAL_DATA';
-export const TOGGLE_LOADING = 'TOGGLE_LOADING';
-export const SET_PARENT_TREE_URL = 'SET_PARENT_TREE_URL';
-export const SET_ROOT = 'SET_ROOT';
-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';
-export const SET_RESIZING_STATUS = 'SET_RESIZING_STATUS';
-
-// 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';
-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';
-
-// Viewer mutation types
-export const SET_PREVIEW_MODE = 'SET_PREVIEW_MODE';
-export const SET_EDIT_MODE = 'SET_EDIT_MODE';
-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/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
deleted file mode 100644
index 03d81be10a1..00000000000
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ /dev/null
@@ -1,70 +0,0 @@
-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';
-
-export default {
- [types.SET_INITIAL_DATA](state, data) {
- Object.assign(state, data);
- },
- [types.SET_PREVIEW_MODE](state) {
- Object.assign(state, {
- currentBlobView: 'repo-preview',
- });
- },
- [types.SET_EDIT_MODE](state) {
- Object.assign(state, {
- currentBlobView: 'repo-editor',
- });
- },
- [types.TOGGLE_LOADING](state, entry) {
- Object.assign(entry, {
- loading: !entry.loading,
- });
- },
- [types.TOGGLE_EDIT_MODE](state) {
- Object.assign(state, {
- editMode: !state.editMode,
- });
- },
- [types.TOGGLE_DISCARD_POPUP](state, discardPopupOpen) {
- Object.assign(state, {
- discardPopupOpen,
- });
- },
- [types.SET_ROOT](state, isRoot) {
- Object.assign(state, {
- isRoot,
- isInitialRoot: isRoot,
- });
- },
- [types.SET_LEFT_PANEL_COLLAPSED](state, collapsed) {
- Object.assign(state, {
- leftPanelCollapsed: collapsed,
- });
- },
- [types.SET_RIGHT_PANEL_COLLAPSED](state, collapsed) {
- Object.assign(state, {
- rightPanelCollapsed: collapsed,
- });
- },
- [types.SET_RESIZING_STATUS](state, resizing) {
- Object.assign(state, {
- panelResizing: resizing,
- });
- },
- [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
deleted file mode 100644
index 04b9582c5bb..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/branch.js
+++ /dev/null
@@ -1,28 +0,0 @@
-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/ide/stores/mutations/file.js b/app/assets/javascripts/ide/stores/mutations/file.js
deleted file mode 100644
index 72db1c180c9..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/file.js
+++ /dev/null
@@ -1,74 +0,0 @@
-import * as types from '../mutation_types';
-import { findIndexOfFile } from '../utils';
-
-export default {
- [types.SET_FILE_ACTIVE](state, { file, active }) {
- Object.assign(file, {
- active,
- });
-
- Object.assign(state, {
- selectedFile: file,
- });
- },
- [types.TOGGLE_FILE_OPEN](state, file) {
- Object.assign(file, {
- opened: !file.opened,
- });
-
- if (file.opened) {
- state.openFiles.push(file);
- } else {
- state.openFiles.splice(findIndexOfFile(state.openFiles, file), 1);
- }
- },
- [types.SET_FILE_DATA](state, { data, file }) {
- Object.assign(file, {
- blamePath: data.blame_path,
- commitsPath: data.commits_path,
- permalink: data.permalink,
- rawPath: data.raw_path,
- binary: data.binary,
- html: data.html,
- renderError: data.render_error,
- });
- },
- [types.SET_FILE_RAW_DATA](state, { file, raw }) {
- Object.assign(file, {
- raw,
- });
- },
- [types.UPDATE_FILE_CONTENT](state, { file, content }) {
- const changed = content !== file.raw;
-
- Object.assign(file, {
- content,
- 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: file.raw,
- changed: false,
- });
- },
- [types.CREATE_TMP_FILE](state, { file, parent }) {
- parent.tree.push(file);
- },
-};
diff --git a/app/assets/javascripts/ide/stores/mutations/project.js b/app/assets/javascripts/ide/stores/mutations/project.js
deleted file mode 100644
index 2816562a919..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/project.js
+++ /dev/null
@@ -1,23 +0,0 @@
-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/ide/stores/mutations/tree.js b/app/assets/javascripts/ide/stores/mutations/tree.js
deleted file mode 100644
index 4fe438ab465..00000000000
--- a/app/assets/javascripts/ide/stores/mutations/tree.js
+++ /dev/null
@@ -1,36 +0,0 @@
-import * as types from '../mutation_types';
-
-export default {
- [types.TOGGLE_TREE_OPEN](state, tree) {
- Object.assign(tree, {
- 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,
- });
- },
- [types.SET_PARENT_TREE_URL](state, url) {
- Object.assign(state, {
- parentTreeUrl: url,
- });
- },
- [types.SET_LAST_COMMIT_URL](state, { tree = state, url }) {
- Object.assign(tree, {
- lastCommitPath: url,
- });
- },
- [types.CREATE_TMP_TREE](state, { parent, tmpEntry }) {
- parent.tree.push(tmpEntry);
- },
-};
diff --git a/app/assets/javascripts/ide/stores/state.js b/app/assets/javascripts/ide/stores/state.js
deleted file mode 100644
index 61d12096946..00000000000
--- a/app/assets/javascripts/ide/stores/state.js
+++ /dev/null
@@ -1,23 +0,0 @@
-export default () => ({
- canCommit: false,
- currentProjectId: '',
- currentBranchId: '',
- currentBlobView: 'repo-editor',
- discardPopupOpen: false,
- editMode: true,
- endpoints: {},
- isRoot: false,
- isInitialRoot: false,
- lastCommitPath: '',
- loading: false,
- onTopOfBranch: false,
- openFiles: [],
- selectedFile: null,
- path: '',
- parentTreeUrl: '',
- trees: {},
- projects: {},
- leftPanelCollapsed: false,
- rightPanelCollapsed: true,
- panelResizing: false,
-});
diff --git a/app/assets/javascripts/ide/stores/utils.js b/app/assets/javascripts/ide/stores/utils.js
deleted file mode 100644
index d556404faa5..00000000000
--- a/app/assets/javascripts/ide/stores/utils.js
+++ /dev/null
@@ -1,177 +0,0 @@
-import _ from 'underscore';
-
-export const dataStructure = () => ({
- id: '',
- key: '',
- type: '',
- projectId: '',
- branchId: '',
- name: '',
- url: '',
- path: '',
- level: 0,
- tempFile: false,
- icon: '',
- tree: [],
- loading: false,
- opened: false,
- active: false,
- changed: false,
- lastCommitPath: '',
- lastCommit: {
- id: '',
- url: '',
- message: '',
- updatedAt: '',
- author: '',
- },
- tree_url: '',
- blamePath: '',
- commitsPath: '',
- permalink: '',
- rawPath: '',
- binary: false,
- html: '',
- raw: '',
- content: '',
- parentTreeUrl: '',
- renderError: false,
- base64: false,
- editorRow: 1,
- editorColumn: 1,
- fileLanguage: '',
- eol: '',
-});
-
-export const decorateData = (entity) => {
- const {
- id,
- projectId,
- branchId,
- type,
- url,
- name,
- icon,
- tree_url,
- path,
- renderError,
- content = '',
- tempFile = false,
- active = false,
- opened = false,
- changed = false,
- parentTreeUrl = '',
- level = 0,
- base64 = false,
- } = entity;
-
- return {
- ...dataStructure(),
- id,
- projectId,
- branchId,
- key: `${name}-${type}-${id}`,
- type,
- name,
- url,
- tree_url,
- path,
- level,
- tempFile,
- icon: `fa-${icon}`,
- opened,
- active,
- parentTreeUrl,
- changed,
- renderError,
- content,
- base64,
- };
-};
-
-/*
- 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 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,
- path: treePath,
- icon: type === 'tree' ? 'folder' : 'file-text-o',
- changed,
- content,
- parentTreeUrl: '',
- level,
- base64,
- renderError: base64,
- url,
- });
-};
-
-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, {
- id: entry.id,
- url: entry.url,
- tempFile: false,
- });
- }
-
- return decorateData({
- ...entry,
- projectId,
- branchId,
- type,
- parentTreeUrl,
- level,
- });
-};
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 5de48aa49a9..9b46bbf83da 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -21,7 +21,7 @@ export default class LabelsSelect {
}
$els.each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
+ var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelUrl, namespacePath, projectPath, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip, initialSelected, $toggleText, fieldName, useId, propertyName, showMenuAbove, $container, $dropdownContainer;
$dropdown = $(dropdown);
$dropdownContainer = $dropdown.closest('.labels-filter');
$toggleText = $dropdown.find('.dropdown-toggle-text');
@@ -53,13 +53,6 @@ export default class LabelsSelect {
.map(function () {
return this.value;
}).get();
- if (issueUpdateURL != null) {
- issueURLSplit = issueUpdateURL.split('/');
- }
- if (issueUpdateURL) {
- labelHTMLTemplate = _.template('<% _.each(labels, function(label){ %> <a href="<%- ["",issueURLSplit[1], issueURLSplit[2],""].join("/") %>issues?label_name[]=<%- encodeURIComponent(label.title) %>"> <span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;"> <%- label.title %> </span> </a> <% }); %>');
- labelNoneHTMLTemplate = '<span class="no-value">None</span>';
- }
const handleClick = options.handleClick;
$sidebarLabelTooltip.tooltip();
@@ -91,14 +84,17 @@ export default class LabelsSelect {
$loading.fadeOut();
$dropdown.trigger('loaded.gl.dropdown');
$selectbox.hide();
- data.issueURLSplit = issueURLSplit;
+ data.issueUpdateURL = issueUpdateURL;
labelCount = 0;
- if (data.labels.length) {
- template = labelHTMLTemplate(data);
+ if (data.labels.length && issueUpdateURL) {
+ template = LabelsSelect.getLabelTemplate({
+ labels: data.labels,
+ issueUpdateURL,
+ });
labelCount = data.labels.length;
}
else {
- template = labelNoneHTMLTemplate;
+ template = '<span class="no-value">None</span>';
}
$value.removeAttr('style').html(template);
$sidebarCollapsedValue.text(labelCount);
@@ -213,7 +209,7 @@ export default class LabelsSelect {
}
}
if (label.duplicate) {
- color = gl.DropdownUtils.duplicateLabelColor(label.color);
+ color = DropdownUtils.duplicateLabelColor(label.color);
}
else {
if (label.color != null) {
@@ -242,10 +238,16 @@ export default class LabelsSelect {
filterable: true,
selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
+ var $dropdownParent = $dropdown.parent();
+ var $dropdownInputField = $dropdownParent.find('.dropdown-input-field');
var isSelected = el !== null ? el.hasClass('is-active') : false;
var title = selected.title;
var selectedLabels = this.selected;
+ if ($dropdownInputField.length && $dropdownInputField.val().length) {
+ $dropdownParent.find('.dropdown-input-clear').trigger('click');
+ }
+
if (selected.id === 0) {
this.selected = [];
return 'No Label';
@@ -412,6 +414,26 @@ export default class LabelsSelect {
this.bindEvents();
}
+ static getLabelTemplate(tplData) {
+ // We could use ES6 template string here
+ // and properly indent markup for readability
+ // but that also introduces unintended white-space
+ // so best approach is to use traditional way of
+ // concatenation
+ // see: http://2ality.com/2016/05/template-literal-whitespace.html#joining-arrays
+ const tpl = _.template([
+ '<% _.each(labels, function(label){ %>',
+ '<a href="<%- issueUpdateURL.slice(0, issueUpdateURL.lastIndexOf("/")) %>?label_name[]=<%- encodeURIComponent(label.title) %>">',
+ '<span class="label has-tooltip color-label" title="<%- label.description %>" style="background-color: <%- label.color %>; color: <%- label.text_color %>;">',
+ '<%- label.title %>',
+ '</span>',
+ '</a>',
+ '<% }); %>',
+ ].join(''));
+
+ return tpl(tplData);
+ }
+
bindEvents() {
return $('body').on('change', '.selected_issue', this.onSelectCheckboxIssue);
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 017f3b986fd..ed90db317df 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -1,3 +1,5 @@
+import jQuery from 'jquery';
+import Cookies from 'js-cookie';
import axios from './axios_utils';
import { getLocationHash } from './url_utility';
import { convertToCamelCase } from './text_utility';
@@ -22,13 +24,18 @@ export const getGroupSlug = () => {
return null;
};
-export const isInIssuePage = () => {
- const page = getPagePath(1);
- const action = getPagePath(2);
+export const checkPageAndAction = (page, action) => {
+ const pagePath = getPagePath(1);
+ const actionPath = getPagePath(2);
- return page === 'issues' && action === 'show';
+ return pagePath === page && actionPath === action;
};
+export const isInIssuePage = () => checkPageAndAction('issues', 'show');
+export const isInMRPage = () => checkPageAndAction('merge_requests', 'show');
+export const isInNoteablePage = () => isInIssuePage() || isInMRPage();
+export const hasVueMRDiscussionsCookie = () => Cookies.get('vue_mr_discussions');
+
export const ajaxGet = url => axios.get(url, {
params: { format: 'js' },
responseType: 'text',
@@ -133,7 +140,11 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const scrollToElement = ($el) => {
+export const scrollToElement = (element) => {
+ let $el = element;
+ if (!(element instanceof jQuery)) {
+ $el = $(element);
+ }
const top = $el.offset().top;
const mrTabsHeight = $('.merge-request-tabs').height() || 0;
const headerHeight = $('.navbar-gitlab').height() || 0;
@@ -291,6 +302,14 @@ export const parseQueryStringIntoObject = (query = '') => {
}, {});
};
+/**
+ * Converts object with key-value pairs
+ * into query-param string
+ *
+ * @param {Object} params
+ */
+export const objectToQueryString = (params = {}) => Object.keys(params).map(param => `${param}=${params[param]}`).join('&');
+
export const buildUrlWithCurrentLocation = param => (param ? `${window.location.pathname}${param}` : window.location.pathname);
/**
diff --git a/app/assets/javascripts/lib/utils/text_utility.js b/app/assets/javascripts/lib/utils/text_utility.js
index 94d03621bff..c0ce0786518 100644
--- a/app/assets/javascripts/lib/utils/text_utility.js
+++ b/app/assets/javascripts/lib/utils/text_utility.js
@@ -65,6 +65,20 @@ export function capitalizeFirstCharacter(text) {
return `${text[0].toUpperCase()}${text.slice(1)}`;
}
+export function camelCase(str) {
+ return str.replace(/_+([a-z])/gi, ($1, $2) => $2.toUpperCase());
+}
+
+export function camelCaseKeys(obj = {}) {
+ return Object.keys(obj).reduce((acc, key) => {
+ const camelKey = camelCase(key);
+ return {
+ ...acc,
+ [camelKey]: obj[key],
+ };
+ }, {});
+}
+
/**
* Replaces all html tags from a string with the given replacement.
*
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 659dc9eaa1f..53b01cca1d3 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -36,8 +36,11 @@ import initBreadcrumbs from './breadcrumb';
import initDispatcher from './dispatcher';
-// eslint-disable-next-line global-require, import/no-commonjs
-if (process.env.NODE_ENV !== 'production') require('./test_utils/');
+// inject test utilities if necessary
+if (process.env.NODE_ENV !== 'production' && gon && gon.test_env) {
+ $.fx.off = true;
+ import(/* webpackMode: "eager" */ './test_utils/');
+}
svg4everybody();
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
index b4b3c15108d..66b258839ae 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflicts_bundle.js
@@ -12,7 +12,7 @@ import './components/inline_conflict_lines';
import './components/parallel_conflict_lines';
import syntaxHighlight from '../syntax_highlight';
-$(() => {
+export default function initMergeConflicts() {
const INTERACTIVE_RESOLVE_MODE = 'interactive';
const conflictsEl = document.querySelector('#conflicts');
const mergeConflictsStore = gl.mergeConflicts.mergeConflictsStore;
@@ -91,4 +91,4 @@ $(() => {
}
}
});
-});
+}
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 41971e92ec0..46789e324c2 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -241,6 +241,10 @@ export default class MergeRequestTabs {
return newState;
}
+ getCurrentAction() {
+ return this.currentAction;
+ }
+
loadCommits(source) {
if (this.commitsLoaded) {
return;
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 2841ecb558b..c259d5405bd 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -216,6 +216,9 @@ export default class MilestoneSelect {
$value.html(milestoneLinkNoneTemplate);
return $sidebarCollapsedValue.find('span').text('No');
}
+ })
+ .catch(() => {
+ $loading.fadeOut();
});
}
}
diff --git a/app/assets/javascripts/mr_notes/index.js b/app/assets/javascripts/mr_notes/index.js
new file mode 100644
index 00000000000..972fdb2b791
--- /dev/null
+++ b/app/assets/javascripts/mr_notes/index.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import notesApp from '../notes/components/notes_app.vue';
+import discussionCounter from '../notes/components/discussion_counter.vue';
+import store from '../notes/stores';
+
+export default function initMrNotes() {
+ new Vue({ // eslint-disable-line
+ el: '#js-vue-mr-discussions',
+ components: {
+ notesApp,
+ },
+ data() {
+ const notesDataset = document.getElementById('js-vue-mr-discussions').dataset;
+ return {
+ noteableData: JSON.parse(notesDataset.noteableData),
+ currentUserData: JSON.parse(notesDataset.currentUserData),
+ notesData: JSON.parse(notesDataset.notesData),
+ };
+ },
+ render(createElement) {
+ return createElement('notes-app', {
+ props: {
+ noteableData: this.noteableData,
+ notesData: this.notesData,
+ userData: this.currentUserData,
+ },
+ });
+ },
+ });
+
+ new Vue({ // eslint-disable-line
+ el: '#js-vue-discussion-counter',
+ components: {
+ discussionCounter,
+ },
+ store,
+ render(createElement) {
+ return createElement('discussion-counter');
+ },
+ });
+}
diff --git a/app/assets/javascripts/network/network_bundle.js b/app/assets/javascripts/network/network_bundle.js
deleted file mode 100644
index 129f1724cb8..00000000000
--- a/app/assets/javascripts/network/network_bundle.js
+++ /dev/null
@@ -1,17 +0,0 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, quotes, no-var, vars-on-top, camelcase, comma-dangle, consistent-return, max-len */
-
-import ShortcutsNetwork from '../shortcuts_network';
-import Network from './network';
-
-$(function() {
- if (!$(".network-graph").length) return;
-
- var network_graph;
- network_graph = new Network({
- url: $(".network-graph").attr('data-url'),
- commit_url: $(".network-graph").attr('data-commit-url'),
- ref: $(".network-graph").attr('data-ref'),
- commit_id: $(".network-graph").attr('data-commit-id')
- });
- return new ShortcutsNetwork(network_graph.branch_graph);
-});
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index f17b432cffd..c640003d958 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -24,7 +24,7 @@ import GLForm from './gl_form';
import loadAwardsHandler from './awards_handler';
import Autosave from './autosave';
import TaskList from './task_list';
-import { isInViewport, getPagePath, scrollToElement, isMetaKey } from './lib/utils/common_utils';
+import { isInViewport, getPagePath, scrollToElement, isMetaKey, hasVueMRDiscussionsCookie } from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
@@ -44,6 +44,10 @@ export default class Notes {
}
}
+ static getInstance() {
+ return this.instance;
+ }
+
constructor(notes_url, note_ids, last_fetched_at, view, enableGFM = true) {
this.updateTargetButtons = this.updateTargetButtons.bind(this);
this.updateComment = this.updateComment.bind(this);
@@ -102,67 +106,77 @@ export default class Notes {
}
addBinding() {
+ this.$wrapperEl = hasVueMRDiscussionsCookie() ? $(document).find('.diffs') : $(document);
+
// Edit note link
- $(document).on('click', '.js-note-edit', this.showEditForm.bind(this));
- $(document).on('click', '.note-edit-cancel', this.cancelEdit);
+ this.$wrapperEl.on('click', '.js-note-edit', this.showEditForm.bind(this));
+ this.$wrapperEl.on('click', '.note-edit-cancel', this.cancelEdit);
// Reopen and close actions for Issue/MR combined with note form submit
- $(document).on('click', '.js-comment-submit-button', this.postComment);
- $(document).on('click', '.js-comment-save-button', this.updateComment);
- $(document).on('keyup input', '.js-note-text', this.updateTargetButtons);
+ this.$wrapperEl.on('click', '.js-comment-submit-button', this.postComment);
+ this.$wrapperEl.on('click', '.js-comment-save-button', this.updateComment);
+ this.$wrapperEl.on('keyup input', '.js-note-text', this.updateTargetButtons);
// resolve a discussion
- $(document).on('click', '.js-comment-resolve-button', this.postComment);
+ this.$wrapperEl.on('click', '.js-comment-resolve-button', this.postComment);
// remove a note (in general)
- $(document).on('click', '.js-note-delete', this.removeNote);
+ this.$wrapperEl.on('click', '.js-note-delete', this.removeNote);
// delete note attachment
- $(document).on('click', '.js-note-attachment-delete', this.removeAttachment);
+ this.$wrapperEl.on('click', '.js-note-attachment-delete', this.removeAttachment);
// reset main target form when clicking discard
- $(document).on('click', '.js-note-discard', this.resetMainTargetForm);
+ this.$wrapperEl.on('click', '.js-note-discard', this.resetMainTargetForm);
// update the file name when an attachment is selected
- $(document).on('change', '.js-note-attachment-input', this.updateFormAttachment);
+ this.$wrapperEl.on('change', '.js-note-attachment-input', this.updateFormAttachment);
// reply to diff/discussion notes
- $(document).on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
+ this.$wrapperEl.on('click', '.js-discussion-reply-button', this.onReplyToDiscussionNote);
// add diff note
- $(document).on('click', '.js-add-diff-note-button', this.onAddDiffNote);
+ this.$wrapperEl.on('click', '.js-add-diff-note-button', this.onAddDiffNote);
// add diff note for images
- $(document).on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
+ this.$wrapperEl.on('click', '.js-add-image-diff-note-button', this.onAddImageDiffNote);
// hide diff note form
- $(document).on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
+ this.$wrapperEl.on('click', '.js-close-discussion-note-form', this.cancelDiscussionForm);
// toggle commit list
- $(document).on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
+ this.$wrapperEl.on('click', '.system-note-commit-list-toggler', this.toggleCommitList);
// fetch notes when tab becomes visible
- $(document).on('visibilitychange', this.visibilityChange);
+ this.$wrapperEl.on('visibilitychange', this.visibilityChange);
// when issue status changes, we need to refresh data
- $(document).on('issuable:change', this.refresh);
+ this.$wrapperEl.on('issuable:change', this.refresh);
// ajax:events that happen on Form when actions like Reopen, Close are performed on Issues and MRs.
- $(document).on('ajax:success', '.js-main-target-form', this.addNote);
- $(document).on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
- $(document).on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
- $(document).on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.addNote);
+ this.$wrapperEl.on('ajax:success', '.js-discussion-note-form', this.addDiscussionNote);
+ this.$wrapperEl.on('ajax:success', '.js-main-target-form', this.resetMainTargetForm);
+ this.$wrapperEl.on('ajax:complete', '.js-main-target-form', this.reenableTargetFormSubmitButton);
// when a key is clicked on the notes
- $(document).on('keydown', '.js-note-text', this.keydownNoteText);
+ this.$wrapperEl.on('keydown', '.js-note-text', this.keydownNoteText);
// When the URL fragment/hash has changed, `#note_xxx`
- return $(window).on('hashchange', this.onHashChange);
+ $(window).on('hashchange', this.onHashChange);
+ this.boundGetContent = this.getContent.bind(this);
+ document.addEventListener('refreshLegacyNotes', this.boundGetContent);
+ this.eventsBound = true;
}
cleanBinding() {
- $(document).off('click', '.js-note-edit');
- $(document).off('click', '.note-edit-cancel');
- $(document).off('click', '.js-note-delete');
- $(document).off('click', '.js-note-attachment-delete');
- $(document).off('click', '.js-discussion-reply-button');
- $(document).off('click', '.js-add-diff-note-button');
- $(document).off('click', '.js-add-image-diff-note-button');
- $(document).off('visibilitychange');
- $(document).off('keyup input', '.js-note-text');
- $(document).off('click', '.js-note-target-reopen');
- $(document).off('click', '.js-note-target-close');
- $(document).off('click', '.js-note-discard');
- $(document).off('keydown', '.js-note-text');
- $(document).off('click', '.js-comment-resolve-button');
- $(document).off('click', '.system-note-commit-list-toggler');
- $(document).off('ajax:success', '.js-main-target-form');
- $(document).off('ajax:success', '.js-discussion-note-form');
- $(document).off('ajax:complete', '.js-main-target-form');
+ if (!this.eventsBound) {
+ return;
+ }
+
+ this.$wrapperEl.off('click', '.js-note-edit');
+ this.$wrapperEl.off('click', '.note-edit-cancel');
+ this.$wrapperEl.off('click', '.js-note-delete');
+ this.$wrapperEl.off('click', '.js-note-attachment-delete');
+ this.$wrapperEl.off('click', '.js-discussion-reply-button');
+ this.$wrapperEl.off('click', '.js-add-diff-note-button');
+ this.$wrapperEl.off('click', '.js-add-image-diff-note-button');
+ this.$wrapperEl.off('visibilitychange');
+ this.$wrapperEl.off('keyup input', '.js-note-text');
+ this.$wrapperEl.off('click', '.js-note-target-reopen');
+ this.$wrapperEl.off('click', '.js-note-target-close');
+ this.$wrapperEl.off('click', '.js-note-discard');
+ this.$wrapperEl.off('keydown', '.js-note-text');
+ this.$wrapperEl.off('click', '.js-comment-resolve-button');
+ this.$wrapperEl.off('click', '.system-note-commit-list-toggler');
+ this.$wrapperEl.off('ajax:success', '.js-main-target-form');
+ this.$wrapperEl.off('ajax:success', '.js-discussion-note-form');
+ this.$wrapperEl.off('ajax:complete', '.js-main-target-form');
+ document.removeEventListener('refreshLegacyNotes', this.boundGetContent);
$(window).off('hashchange', this.onHashChange);
}
@@ -252,8 +266,10 @@ export default class Notes {
if (this.refreshing) {
return;
}
+
this.refreshing = true;
- axios.get(this.notes_url, {
+
+ axios.get(`${this.notes_url}?html=true`, {
headers: {
'X-Last-Fetched-At': this.last_fetched_at,
},
@@ -350,7 +366,7 @@ export default class Notes {
}
if (!noteEntity.valid) {
- if (noteEntity.errors.commands_only) {
+ if (noteEntity.errors && noteEntity.errors.commands_only) {
if (noteEntity.commands_changes &&
Object.keys(noteEntity.commands_changes).length > 0) {
$notesList.find('.system-note.being-posted').remove();
@@ -363,6 +379,10 @@ export default class Notes {
const $note = $notesList.find(`#note_${noteEntity.id}`);
if (Notes.isNewNote(noteEntity, this.note_ids)) {
+ if (hasVueMRDiscussionsCookie()) {
+ return;
+ }
+
this.note_ids.push(noteEntity.id);
if ($notesList.length) {
@@ -399,6 +419,8 @@ export default class Notes {
this.setupNewNote($updatedNote);
}
}
+
+ Notes.refreshVueNotes();
}
isParallelView() {
@@ -406,12 +428,11 @@ export default class Notes {
}
/**
- * Render note in discussion area.
- *
- * Note: for rendering inline notes use renderDiscussionNote
+ * Render note in discussion area. To render inline notes use renderDiscussionNote.
*/
renderDiscussionNote(noteEntity, $form) {
var discussionContainer, form, row, lineType, diffAvatarContainer;
+
if (!Notes.isNewNote(noteEntity, this.note_ids)) {
return;
}
@@ -452,7 +473,9 @@ export default class Notes {
// Init discussion on 'Discussion' page if it is merge request page
const page = $('body').attr('data-page');
if ((page && page.indexOf('projects:merge_request') !== -1) || !noteEntity.diff_discussion_html) {
- Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
+ if (!hasVueMRDiscussionsCookie()) {
+ Notes.animateAppendNote(noteEntity.discussion_html, $('.main-notes-list'));
+ }
}
} else {
// append new note to all matching discussions
@@ -634,7 +657,6 @@ export default class Notes {
var $noteEntityEl, $note_li;
// Convert returned HTML to a jQuery object so we can modify it further
$noteEntityEl = $(noteEntity.html);
- $noteEntityEl.addClass('fade-in-full');
this.revertNoteEditForm($targetNote);
$noteEntityEl.renderGFM();
// Find the note's `li` element by ID and replace it with the updated HTML
@@ -730,7 +752,7 @@ export default class Notes {
var selector = this.getEditFormSelector($target);
var $editForm = $(selector);
- $editForm.insertBefore('.notes-form');
+ $editForm.insertBefore('.diffs');
$editForm.find('.js-comment-save-button').enable();
$editForm.find('.js-finish-edit-warning').hide();
}
@@ -746,7 +768,8 @@ export default class Notes {
}
removeNoteEditForm($note) {
- var form = $note.find('.current-note-edit-form');
+ var form = $note.find('.diffs .current-note-edit-form');
+
$note.removeClass('is-editing');
form.removeClass('current-note-edit-form');
form.find('.js-finish-edit-warning').hide();
@@ -818,6 +841,7 @@ export default class Notes {
};
})(this));
+ Notes.refreshVueNotes();
Notes.checkMergeRequestStatus();
return this.updateNotesCount(-1);
}
@@ -1157,7 +1181,7 @@ export default class Notes {
this.glForm = new GLForm($editForm.find('form'), this.enableGFM);
$editForm.find('form')
- .attr('action', postUrl)
+ .attr('action', `${postUrl}?html=true`)
.attr('data-remote', 'true');
$editForm.find('.js-form-target-id').val(targetId);
$editForm.find('.js-form-target-type').val(targetType);
@@ -1280,6 +1304,10 @@ export default class Notes {
return $updatedNote;
}
+ static refreshVueNotes() {
+ document.dispatchEvent(new CustomEvent('refreshVueNotes'));
+ }
+
/**
* Get data from Form attributes to use for saving/submitting comment.
*/
@@ -1481,7 +1509,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to submit comment on server
- axios.post(formAction, formData)
+ axios.post(`${formAction}?html=true`, formData)
.then((res) => {
const note = res.data;
@@ -1546,6 +1574,8 @@ export default class Notes {
if ($notesContainer.length) {
$notesContainer.append('<div class="flash-container" style="display: none;"></div>');
}
+
+ Notes.refreshVueNotes();
} else if (isMainForm) { // Check if this was main thread comment
// Show final note element on UI and perform form and action buttons cleanup
this.addNote($form, note);
@@ -1627,7 +1657,7 @@ export default class Notes {
/* eslint-disable promise/catch-or-return */
// Make request to update comment on server
- axios.post(formAction, formData)
+ axios.post(`${formAction}?html=true`, formData)
.then(({ data }) => {
// Submission successful! render final note element
this.updateNote(data, $editingNote);
diff --git a/app/assets/javascripts/notes/components/comment_form.vue b/app/assets/javascripts/notes/components/comment_form.vue
index df796050e0d..b85c1a6ad72 100644
--- a/app/assets/javascripts/notes/components/comment_form.vue
+++ b/app/assets/javascripts/notes/components/comment_form.vue
@@ -2,10 +2,11 @@
import { mapActions, mapGetters } from 'vuex';
import _ from 'underscore';
import Autosize from 'autosize';
- import { __ } from '~/locale';
+ import { __, sprintf } from '~/locale';
import Flash from '../../flash';
import Autosave from '../../autosave';
import TaskList from '../../task_list';
+ import { capitalizeFirstCharacter, convertToCamelCase } from '../../lib/utils/text_utility';
import * as constants from '../constants';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
@@ -29,6 +30,12 @@
mixins: [
issuableStateMixin,
],
+ props: {
+ noteableType: {
+ type: String,
+ required: true,
+ },
+ },
data() {
return {
note: '',
@@ -43,37 +50,51 @@
'getUserData',
'getNoteableData',
'getNotesData',
- 'issueState',
+ 'openState',
]),
+ noteableDisplayName() {
+ return this.noteableType.replace(/_/g, ' ');
+ },
isLoggedIn() {
return this.getUserData.id;
},
commentButtonTitle() {
return this.noteType === constants.COMMENT ? 'Comment' : 'Start discussion';
},
- isIssueOpen() {
- return this.issueState === constants.OPENED || this.issueState === constants.REOPENED;
+ isOpen() {
+ return this.openState === constants.OPENED || this.openState === constants.REOPENED;
},
canCreateNote() {
return this.getNoteableData.current_user.can_create_note;
},
issueActionButtonTitle() {
- if (this.note.length) {
- const actionText = this.isIssueOpen ? 'close' : 'reopen';
+ const openOrClose = this.isOpen ? 'close' : 'reopen';
- return this.noteType === constants.COMMENT ?
- `Comment & ${actionText} issue` :
- `Start discussion & ${actionText} issue`;
+ if (this.note.length) {
+ return sprintf(
+ __('%{actionText} & %{openOrClose} %{noteable}'),
+ {
+ actionText: this.commentButtonTitle,
+ openOrClose,
+ noteable: this.noteableDisplayName,
+ },
+ );
}
- return this.isIssueOpen ? 'Close issue' : 'Reopen issue';
+ return sprintf(
+ __('%{openOrClose} %{noteable}'),
+ {
+ openOrClose: capitalizeFirstCharacter(openOrClose),
+ noteable: this.noteableDisplayName,
+ },
+ );
},
actionButtonClassNames() {
return {
- 'btn-reopen': !this.isIssueOpen,
- 'btn-close': this.isIssueOpen,
- 'js-note-target-close': this.isIssueOpen,
- 'js-note-target-reopen': !this.isIssueOpen,
+ 'btn-reopen': !this.isOpen,
+ 'btn-close': this.isOpen,
+ 'js-note-target-close': this.isOpen,
+ 'js-note-target-reopen': !this.isOpen,
};
},
markdownDocsPath() {
@@ -138,7 +159,7 @@
flashContainer: this.$el,
data: {
note: {
- noteable_type: constants.NOTEABLE_TYPE,
+ noteable_type: this.noteableType,
noteable_id: this.getNoteableData.id,
note: this.note,
},
@@ -193,19 +214,29 @@ Please check your network connection and try again.`;
this.isSubmitting = false;
},
toggleIssueState() {
- if (this.isIssueOpen) {
+ if (this.isOpen) {
this.closeIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
- Flash(__('Something went wrong while closing the issue. Please try again later'));
+ Flash(
+ sprintf(
+ __('Something went wrong while closing the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
+ ),
+ );
});
} else {
this.reopenIssue()
.then(() => this.enableButton())
.catch(() => {
this.enableButton();
- Flash(__('Something went wrong while reopening the issue. Please try again later'));
+ Flash(
+ sprintf(
+ __('Something went wrong while reopening the %{issuable}. Please try again later'),
+ { issuable: this.noteableDisplayName },
+ ),
+ );
});
}
},
@@ -221,7 +252,6 @@ Please check your network connection and try again.`;
this.$refs.markdownField.previewMarkdown = false;
}
- // reset autostave
this.autosave.reset();
},
setNoteType(type) {
@@ -240,10 +270,11 @@ Please check your network connection and try again.`;
},
initAutoSave() {
if (this.isLoggedIn) {
+ const noteableType = capitalizeFirstCharacter(convertToCamelCase(this.noteableType));
+
this.autosave = new Autosave(
$(this.$refs.textarea),
- ['Note', 'Issue', this.getNoteableData.id],
- 'issue',
+ ['Note', noteableType, this.getNoteableData.id],
);
}
},
@@ -331,7 +362,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
:disabled="isSubmitButtonDisabled"
class="btn btn-create comment-btn js-comment-button js-comment-submit-button"
type="submit">
- {{ commentButtonTitle }}
+ {{ __(commentButtonTitle) }}
</button>
<button
:disabled="isSubmitButtonDisabled"
@@ -359,7 +390,7 @@ append-right-10 comment-type-dropdown js-comment-type-dropdown droplab-dropdown"
<div class="description">
<strong>Comment</strong>
<p>
- Add a general comment to this issue.
+ Add a general comment to this {{ noteableDisplayName }}.
</p>
</div>
</button>
diff --git a/app/assets/javascripts/notes/components/diff_file_header.vue b/app/assets/javascripts/notes/components/diff_file_header.vue
new file mode 100644
index 00000000000..fe5baa3537f
--- /dev/null
+++ b/app/assets/javascripts/notes/components/diff_file_header.vue
@@ -0,0 +1,92 @@
+<script>
+ import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+ import Icon from '~/vue_shared/components/icon.vue';
+
+ export default {
+ components: {
+ ClipboardButton,
+ Icon,
+ },
+ props: {
+ diffFile: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ titleTag() {
+ return this.diffFile.discussionPath ? 'a' : 'span';
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="file-header-content">
+ <div
+ v-if="diffFile.submodule"
+ >
+ <span>
+ <icon name="archive" />
+ <strong
+ v-html="diffFile.submoduleLink"
+ class="file-title-name"
+ ></strong>
+ <clipboard-button
+ title="Copy file path to clipboard"
+ :text="diffFile.submoduleLink"
+ />
+ </span>
+ </div>
+ <template v-else>
+ <component
+ ref="titleWrapper"
+ :is="titleTag"
+ :href="diffFile.discussionPath"
+ >
+ <span v-html="diffFile.blobIcon"></span>
+ <span v-if="diffFile.renamedFile">
+ <strong
+ class="file-title-name has-tooltip"
+ :title="diffFile.oldPath"
+ data-container="body"
+ >
+ {{ diffFile.oldPath }}
+ </strong>
+ &rarr;
+ <strong
+ class="file-title-name has-tooltip"
+ :title="diffFile.newPath"
+ data-container="body"
+ >
+ {{ diffFile.newPath }}
+ </strong>
+ </span>
+
+ <strong
+ v-else
+ class="file-title-name has-tooltip"
+ :title="diffFile.oldPath"
+ data-container="body"
+ >
+ {{ diffFile.filePath }}
+ <span v-if="diffFile.deletedFile">
+ deleted
+ </span>
+ </strong>
+ </component>
+
+ <clipboard-button
+ title="Copy file path to clipboard"
+ :text="diffFile.filePath"
+ />
+
+ <small
+ v-if="diffFile.modeChanged"
+ ref="fileMode"
+ >
+ {{ diffFile.aMode }} → {{ diffFile.bMode }}
+ </small>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/diff_with_note.vue b/app/assets/javascripts/notes/components/diff_with_note.vue
new file mode 100644
index 00000000000..75a32709ad5
--- /dev/null
+++ b/app/assets/javascripts/notes/components/diff_with_note.vue
@@ -0,0 +1,96 @@
+<script>
+ import syntaxHighlight from '~/syntax_highlight';
+ import imageDiffHelper from '~/image_diff/helpers/index';
+ import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+ import DiffFileHeader from './diff_file_header.vue';
+
+ export default {
+ components: {
+ DiffFileHeader,
+ },
+ props: {
+ discussion: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ isImageDiff() {
+ return !this.diffFile.text;
+ },
+ diffFileClass() {
+ const { text } = this.diffFile;
+ return text ? 'text-file' : 'js-image-file';
+ },
+ diffRows() {
+ return $(this.discussion.truncatedDiffLines);
+ },
+ diffFile() {
+ return convertObjectPropsToCamelCase(this.discussion.diffFile);
+ },
+ imageDiffHtml() {
+ return this.discussion.imageDiffHtml;
+ },
+ },
+ mounted() {
+ if (this.isImageDiff) {
+ const canCreateNote = false;
+ const renderCommentBadge = true;
+ imageDiffHelper.initImageDiff(this.$refs.fileHolder, canCreateNote, renderCommentBadge);
+ } else {
+ const fileHolder = $(this.$refs.fileHolder);
+ this.$nextTick(() => {
+ syntaxHighlight(fileHolder);
+ });
+ }
+ },
+ methods: {
+ rowTag(html) {
+ return html.outerHTML ? 'tr' : 'template';
+ },
+ },
+ };
+</script>
+
+<template>
+ <div
+ ref="fileHolder"
+ class="diff-file file-holder"
+ :class="diffFileClass"
+ >
+ <div class="js-file-title file-title file-title-flex-parent">
+ <diff-file-header
+ :diff-file="diffFile"
+ />
+ </div>
+ <div
+ v-if="diffFile.text"
+ class="diff-content code js-syntax-highlight"
+ >
+ <table>
+ <component
+ :is="rowTag(html)"
+ :class="html.className"
+ v-for="(html, index) in diffRows"
+ v-html="html.outerHTML"
+ :key="index"
+ />
+ <tr class="notes_holder">
+ <td
+ class="notes_line"
+ colspan="2"
+ ></td>
+ <td class="notes_content">
+ <slot></slot>
+ </td>
+ </tr>
+ </table>
+ </div>
+ <div
+ v-else
+ >
+ <div v-html="imageDiffHtml"></div>
+ <slot></slot>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/discussion_counter.vue b/app/assets/javascripts/notes/components/discussion_counter.vue
new file mode 100644
index 00000000000..0158f58b569
--- /dev/null
+++ b/app/assets/javascripts/notes/components/discussion_counter.vue
@@ -0,0 +1,119 @@
+<script>
+ import { mapGetters } from 'vuex';
+ import resolveSvg from 'icons/_icon_resolve_discussion.svg';
+ import resolvedSvg from 'icons/_icon_status_success_solid.svg';
+ import mrIssueSvg from 'icons/_icon_mr_issue.svg';
+ import nextDiscussionSvg from 'icons/_next_discussion.svg';
+ import { pluralize } from '../../lib/utils/text_utility';
+ import { scrollToElement } from '../../lib/utils/common_utils';
+ import tooltip from '../../vue_shared/directives/tooltip';
+
+ export default {
+ directives: {
+ tooltip,
+ },
+ computed: {
+ ...mapGetters([
+ 'getUserData',
+ 'getNoteableData',
+ 'discussionCount',
+ 'unresolvedDiscussions',
+ 'resolvedDiscussionCount',
+ ]),
+ isLoggedIn() {
+ return this.getUserData.id;
+ },
+ hasNextButton() {
+ return this.isLoggedIn && !this.allResolved;
+ },
+ countText() {
+ return pluralize('discussion', this.discussionCount);
+ },
+ allResolved() {
+ return this.resolvedDiscussionCount === this.discussionCount;
+ },
+ resolveAllDiscussionsIssuePath() {
+ return this.getNoteableData.create_issue_to_resolve_discussions_path;
+ },
+ firstUnresolvedDiscussionId() {
+ const item = this.unresolvedDiscussions[0] || {};
+
+ return item.id;
+ },
+ },
+ created() {
+ this.resolveSvg = resolveSvg;
+ this.resolvedSvg = resolvedSvg;
+ this.mrIssueSvg = mrIssueSvg;
+ this.nextDiscussionSvg = nextDiscussionSvg;
+ },
+ methods: {
+ jumpToFirstDiscussion() {
+ const el = document.querySelector(`[data-discussion-id="${this.firstUnresolvedDiscussionId}"]`);
+ const activeTab = window.mrTabs.currentAction;
+
+ if (activeTab === 'commits' || activeTab === 'pipelines') {
+ window.mrTabs.activateTab('show');
+ }
+
+ if (el) {
+ scrollToElement(el);
+ }
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="line-resolve-all-container prepend-top-10">
+ <div>
+ <div
+ v-if="discussionCount > 0"
+ :class="{ 'has-next-btn': hasNextButton }"
+ class="line-resolve-all">
+ <span
+ :class="{ 'is-active': allResolved }"
+ class="line-resolve-btn is-disabled"
+ type="button">
+ <span
+ v-if="allResolved"
+ v-html="resolvedSvg"
+ ></span>
+ <span
+ v-else
+ v-html="resolveSvg"
+ ></span>
+ </span>
+ <span class=".line-resolve-text">
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ countText }} resolved
+ </span>
+ </div>
+ <div
+ v-if="resolveAllDiscussionsIssuePath && !allResolved"
+ class="btn-group"
+ role="group">
+ <a
+ :href="resolveAllDiscussionsIssuePath"
+ v-tooltip
+ title="Resolve all discussions in new issue"
+ data-container="body"
+ class="new-issue-for-discussion btn btn-default discussion-create-issue-btn">
+ <span v-html="mrIssueSvg"></span>
+ </a>
+ </div>
+ <div
+ v-if="isLoggedIn && !allResolved"
+ class="btn-group"
+ role="group">
+ <button
+ @click="jumpToFirstDiscussion"
+ v-tooltip
+ title="Jump to first unresolved discussion"
+ data-container="body"
+ class="btn btn-default discussion-next-btn">
+ <span v-html="nextDiscussionSvg"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/notes/components/note_actions.vue b/app/assets/javascripts/notes/components/note_actions.vue
index 46ffb60aa60..c26aa6fa15d 100644
--- a/app/assets/javascripts/notes/components/note_actions.vue
+++ b/app/assets/javascripts/notes/components/note_actions.vue
@@ -4,6 +4,8 @@
import emojiSmile from 'icons/_emoji_smile.svg';
import emojiSmiley from 'icons/_emoji_smiley.svg';
import editSvg from 'icons/_icon_pencil.svg';
+ import resolveDiscussionSvg from 'icons/_icon_resolve_discussion.svg';
+ import resolvedDiscussionSvg from 'icons/_icon_status_success_solid.svg';
import ellipsisSvg from 'icons/_ellipsis_v.svg';
import loadingIcon from '~/vue_shared/components/loading_icon.vue';
import tooltip from '~/vue_shared/directives/tooltip';
@@ -42,6 +44,26 @@
type: Boolean,
required: true,
},
+ resolvable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResolved: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ isResolving: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ resolvedBy: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
canReportAsAbuse: {
type: Boolean,
required: true,
@@ -63,6 +85,15 @@
currentUserId() {
return this.getUserDataByProp('id');
},
+ resolveButtonTitle() {
+ let title = 'Mark as resolved';
+
+ if (this.resolvedBy) {
+ title = `Resolved by ${this.resolvedBy.name}`;
+ }
+
+ return title;
+ },
},
created() {
this.emojiSmiling = emojiSmiling;
@@ -70,6 +101,8 @@
this.emojiSmiley = emojiSmiley;
this.editSvg = editSvg;
this.ellipsisSvg = ellipsisSvg;
+ this.resolveDiscussionSvg = resolveDiscussionSvg;
+ this.resolvedDiscussionSvg = resolvedDiscussionSvg;
},
methods: {
onEdit() {
@@ -78,6 +111,9 @@
onDelete() {
this.$emit('handleDelete');
},
+ onResolve() {
+ this.$emit('handleResolve');
+ },
},
};
</script>
@@ -90,6 +126,31 @@
{{ accessLevel }}
</span>
<div
+ v-if="resolvable"
+ class="note-actions-item">
+ <button
+ v-tooltip
+ @click="onResolve"
+ :class="{ 'is-disabled': !resolvable, 'is-active': isResolved }"
+ :title="resolveButtonTitle"
+ :aria-label="resolveButtonTitle"
+ type="button"
+ class="line-resolve-btn note-action-button">
+ <template v-if="!isResolving">
+ <div
+ v-if="isResolved"
+ v-html="resolvedDiscussionSvg"></div>
+ <div
+ v-else
+ v-html="resolveDiscussionSvg"></div>
+ </template>
+ <loading-icon
+ v-else
+ :inline="true"
+ />
+ </button>
+ </div>
+ <div
v-if="canAddAwardEmoji"
class="note-actions-item">
<a
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 2d7cd30115d..ca12df9db64 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -41,7 +41,7 @@
this.initTaskList();
if (this.isEditing) {
- this.initAutoSave();
+ this.initAutoSave(this.note.noteable_type);
}
},
updated() {
@@ -50,7 +50,7 @@
if (this.isEditing) {
if (!this.autosave) {
- this.initAutoSave();
+ this.initAutoSave(this.note.noteable_type);
} else {
this.setAutoSave();
}
diff --git a/app/assets/javascripts/notes/components/note_form.vue b/app/assets/javascripts/notes/components/note_form.vue
index d382a9bb642..1a13fdbeb7c 100644
--- a/app/assets/javascripts/notes/components/note_form.vue
+++ b/app/assets/javascripts/notes/components/note_form.vue
@@ -1,9 +1,10 @@
<script>
- import { mapGetters } from 'vuex';
+ import { mapGetters, mapActions } from 'vuex';
import eventHub from '../event_hub';
import issueWarning from '../../vue_shared/components/issue/issue_warning.vue';
import markdownField from '../../vue_shared/components/markdown/field.vue';
import issuableStateMixin from '../mixins/issuable_state';
+ import resolvable from '../mixins/resolvable';
export default {
name: 'IssueNoteForm',
@@ -13,6 +14,7 @@
},
mixins: [
issuableStateMixin,
+ resolvable,
],
props: {
noteBody: {
@@ -30,7 +32,7 @@
required: false,
default: 'Save comment',
},
- discussion: {
+ note: {
type: Object,
required: false,
default: () => ({}),
@@ -42,9 +44,11 @@
},
data() {
return {
- note: this.noteBody,
+ updatedNoteBody: this.noteBody,
conflictWhileEditing: false,
isSubmitting: false,
+ isResolving: false,
+ resolveAsThread: true,
};
},
computed: {
@@ -71,13 +75,13 @@
return this.getUserDataByProp('id');
},
isDisabled() {
- return !this.note.length || this.isSubmitting;
+ return !this.updatedNoteBody.length || this.isSubmitting;
},
},
watch: {
noteBody() {
- if (this.note === this.noteBody) {
- this.note = this.noteBody;
+ if (this.updatedNoteBody === this.noteBody) {
+ this.updatedNoteBody = this.noteBody;
} else {
this.conflictWhileEditing = true;
}
@@ -87,16 +91,24 @@
this.$refs.textarea.focus();
},
methods: {
- handleUpdate() {
+ ...mapActions([
+ 'toggleResolveNote',
+ ]),
+ handleUpdate(shouldResolve) {
+ const beforeSubmitDiscussionState = this.discussionResolved;
this.isSubmitting = true;
- this.$emit('handleFormUpdate', this.note, this.$refs.editNoteForm, () => {
+ this.$emit('handleFormUpdate', this.updatedNoteBody, this.$refs.editNoteForm, () => {
this.isSubmitting = false;
+
+ if (shouldResolve) {
+ this.resolveHandler(beforeSubmitDiscussionState);
+ }
});
},
editMyLastNote() {
- if (this.note === '') {
- const lastNoteInDiscussion = this.getDiscussionLastNote(this.discussion);
+ if (this.updatedNoteBody === '') {
+ const lastNoteInDiscussion = this.getDiscussionLastNote(this.updatedNoteBody);
if (lastNoteInDiscussion) {
eventHub.$emit('enterEditMode', {
@@ -107,7 +119,7 @@
},
cancelHandler(shouldConfirm = false) {
// Sends information about confirm message and if the textarea has changed
- this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.note);
+ this.$emit('cancelFormEdition', shouldConfirm, this.noteBody !== this.updatedNoteBody);
},
},
};
@@ -150,7 +162,7 @@
js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
:data-supports-quick-actions="!isEditing"
aria-label="Description"
- v-model="note"
+ v-model="updatedNoteBody"
ref="textarea"
slot="textarea"
placeholder="Write a comment or drag your files here..."
@@ -169,6 +181,13 @@ js-autosize markdown-area js-vue-issue-note-form js-vue-textarea"
{{ saveButtonTitle }}
</button>
<button
+ v-if="note.resolvable"
+ @click.prevent="handleUpdate(true)"
+ class="btn btn-nr btn-default append-right-10 js-comment-resolve-button"
+ >
+ {{ resolveButtonTitle }}
+ </button>
+ <button
@click="cancelHandler()"
class="btn btn-cancel note-edit-cancel"
type="button">
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 5b255d4a710..4743d95b951 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -34,15 +34,15 @@
required: false,
default: false,
},
- },
- data() {
- return {
- isExpanded: true,
- };
+ expanded: {
+ type: Boolean,
+ required: false,
+ default: true,
+ },
},
computed: {
toggleChevronClass() {
- return this.isExpanded ? 'fa-chevron-up' : 'fa-chevron-down';
+ return this.expanded ? 'fa-chevron-up' : 'fa-chevron-down';
},
noteTimestampLink() {
return `#note_${this.noteId}`;
@@ -53,7 +53,6 @@
'setTargetNoteHash',
]),
handleToggle() {
- this.isExpanded = !this.isExpanded;
this.$emit('toggleHandler');
},
updateTargetNoteHash() {
diff --git a/app/assets/javascripts/notes/components/noteable_discussion.vue b/app/assets/javascripts/notes/components/noteable_discussion.vue
index 98a06c5fc71..76bb53eaf2f 100644
--- a/app/assets/javascripts/notes/components/noteable_discussion.vue
+++ b/app/assets/javascripts/notes/components/noteable_discussion.vue
@@ -1,5 +1,7 @@
<script>
import { mapActions, mapGetters } from 'vuex';
+ import resolveDiscussionsSvg from 'icons/_icon_mr_issue.svg';
+ import nextDiscussionsSvg from 'icons/_next_discussion.svg';
import Flash from '../../flash';
import { SYSTEM_NOTE } from '../constants';
import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
@@ -8,13 +10,19 @@
import noteSignedOutWidget from './note_signed_out_widget.vue';
import noteEditedText from './note_edited_text.vue';
import noteForm from './note_form.vue';
+ import diffWithNote from './diff_with_note.vue';
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import autosave from '../mixins/autosave';
+ import noteable from '../mixins/noteable';
+ import resolvable from '../mixins/resolvable';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import { scrollToElement } from '../../lib/utils/common_utils';
export default {
components: {
noteableNote,
+ diffWithNote,
userAvatarLink,
noteHeader,
noteSignedOutWidget,
@@ -23,8 +31,13 @@
placeholderNote,
placeholderSystemNote,
},
+ directives: {
+ tooltip,
+ },
mixins: [
autosave,
+ noteable,
+ resolvable,
],
props: {
note: {
@@ -35,14 +48,25 @@
data() {
return {
isReplying: false,
+ isResolving: false,
+ resolveAsThread: true,
};
},
computed: {
...mapGetters([
'getNoteableData',
+ 'discussionCount',
+ 'resolvedDiscussionCount',
+ 'unresolvedDiscussions',
]),
discussion() {
- return this.note.notes[0];
+ return {
+ ...this.note.notes[0],
+ truncatedDiffLines: this.note.truncated_diff_lines,
+ diffFile: this.note.diff_file,
+ diffDiscussion: this.note.diff_discussion,
+ imageDiffHtml: this.note.image_diff_html,
+ };
},
author() {
return this.discussion.author;
@@ -71,26 +95,40 @@
return null;
},
+ hasUnresolvedDiscussion() {
+ return this.unresolvedDiscussions.length > 0;
+ },
+ wrapperComponent() {
+ return (this.discussion.diffDiscussion && this.discussion.diffFile) ? diffWithNote : 'div';
+ },
+ wrapperClass() {
+ return this.isDiffDiscussion ? '' : 'panel panel-default';
+ },
},
mounted() {
if (this.isReplying) {
- this.initAutoSave();
+ this.initAutoSave(this.discussion.noteable_type);
}
},
updated() {
if (this.isReplying) {
if (!this.autosave) {
- this.initAutoSave();
+ this.initAutoSave(this.discussion.noteable_type);
} else {
this.setAutoSave();
}
}
},
+ created() {
+ this.resolveDiscussionsSvg = resolveDiscussionsSvg;
+ this.nextDiscussionsSvg = nextDiscussionsSvg;
+ },
methods: {
...mapActions([
'saveNote',
'toggleDiscussion',
'removePlaceholderNotes',
+ 'toggleResolveNote',
]),
componentName(note) {
if (note.isPlaceholderNote) {
@@ -103,7 +141,7 @@
return noteableNote;
},
componentData(note) {
- return note.isPlaceholderNote ? note.notes[0] : note;
+ return note.isPlaceholderNote ? this.note.notes[0] : note;
},
toggleDiscussionHandler() {
this.toggleDiscussion({ discussionId: this.note.id });
@@ -128,7 +166,7 @@
flashContainer: this.$el,
data: {
in_reply_to_discussion_id: this.note.reply_id,
- target_type: 'issue',
+ target_type: this.noteableType,
target_id: this.discussion.noteable_id,
note: { note: noteText },
},
@@ -152,12 +190,27 @@ Please check your network connection and try again.`;
});
});
},
+ jumpToDiscussion() {
+ const unresolvedIds = this.unresolvedDiscussions.map(d => d.id);
+ const index = unresolvedIds.indexOf(this.note.id);
+
+ if (index >= 0 && index !== unresolvedIds.length) {
+ const nextId = unresolvedIds[index + 1];
+ const el = document.querySelector(`[data-discussion-id="${nextId}"]`);
+
+ if (el) {
+ scrollToElement(el);
+ }
+ }
+ },
},
};
</script>
<template>
- <li class="note note-discussion timeline-entry">
+ <li
+ :data-discussion-id="note.id"
+ class="note note-discussion timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
<user-avatar-link
@@ -175,6 +228,7 @@ Please check your network connection and try again.`;
:created-at="discussion.created_at"
:note-id="discussion.id"
:include-toggle="true"
+ :expanded="note.expanded"
@toggleHandler="toggleDiscussionHandler"
action-text="started a discussion"
class="discussion"
@@ -187,43 +241,103 @@ Please check your network connection and try again.`;
class-name="discussion-headline-light js-discussion-headline"
/>
</div>
- </div>
- <div
- v-if="note.expanded"
- class="discussion-body">
- <div class="panel panel-default">
- <div class="discussion-notes">
- <ul class="notes">
- <component
- v-for="note in note.notes"
- :is="componentName(note)"
- :note="componentData(note)"
- :key="note.id"
- />
- </ul>
- <div
- :class="{ 'is-replying': isReplying }"
- class="discussion-reply-holder">
- <button
- v-if="canReply && !isReplying"
- @click="showReplyForm"
- type="button"
- class="js-vue-discussion-reply btn btn-text-field"
- title="Add a reply">
- Reply...
- </button>
- <note-form
- v-if="isReplying"
- save-button-title="Comment"
- :discussion="note"
- :is-editing="false"
- @handleFormUpdate="saveReply"
- @cancelFormEdition="cancelReplyForm"
- ref="noteForm"
- />
- <note-signed-out-widget v-if="!canReply" />
+ <div
+ v-if="note.expanded"
+ class="discussion-body">
+ <component
+ :is="wrapperComponent"
+ :discussion="discussion"
+ :class="wrapperClass"
+ >
+ <div class="discussion-notes">
+ <ul class="notes">
+ <component
+ v-for="note in note.notes"
+ :is="componentName(note)"
+ :note="componentData(note)"
+ :key="note.id"
+ />
+ </ul>
+ <div
+ :class="{ 'is-replying': isReplying }"
+ class="discussion-reply-holder">
+ <template v-if="!isReplying && canReply">
+ <div
+ class="btn-group-justified discussion-with-resolve-btn"
+ role="group">
+ <div
+ class="btn-group"
+ role="group">
+ <button
+ @click="showReplyForm"
+ type="button"
+ class="js-vue-discussion-reply btn btn-text-field"
+ title="Add a reply">Reply...</button>
+ </div>
+ <div
+ v-if="note.resolvable"
+ class="btn-group"
+ role="group">
+ <button
+ @click="resolveHandler()"
+ type="button"
+ class="btn btn-default"
+ >
+ <i
+ v-if="isResolving"
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin"
+ ></i>
+ {{ resolveButtonTitle }}
+ </button>
+ </div>
+ <div
+ class="btn-group discussion-actions"
+ role="group">
+ <div
+ v-if="note.resolvable && !discussionResolved"
+ class="btn-group"
+ role="group">
+ <a
+ :href="note.resolve_with_issue_path"
+ v-tooltip
+ class="new-issue-for-discussion btn
+ btn-default discussion-create-issue-btn"
+ title="Resolve this discussion in a new issue"
+ data-container="body"
+ >
+ <span v-html="resolveDiscussionsSvg"></span>
+ </a>
+ </div>
+ <div
+ v-if="hasUnresolvedDiscussion"
+ class="btn-group"
+ role="group">
+ <button
+ @click="jumpToDiscussion"
+ v-tooltip
+ class="btn btn-default discussion-next-btn"
+ title="Jump to next unresolved discussion"
+ data-container="body"
+ >
+ <span v-html="nextDiscussionsSvg"></span>
+ </button>
+ </div>
+ </div>
+ </div>
+ </template>
+ <note-form
+ v-if="isReplying"
+ save-button-title="Comment"
+ :note="note"
+ :is-editing="false"
+ @handleFormUpdate="saveReply"
+ @cancelFormEdition="cancelReplyForm"
+ ref="noteForm" />
+ <note-signed-out-widget v-if="!canReply" />
+ </div>
</div>
- </div>
+ </component>
</div>
</div>
</div>
diff --git a/app/assets/javascripts/notes/components/noteable_note.vue b/app/assets/javascripts/notes/components/noteable_note.vue
index 045077de383..4d17bd5acc2 100644
--- a/app/assets/javascripts/notes/components/noteable_note.vue
+++ b/app/assets/javascripts/notes/components/noteable_note.vue
@@ -7,6 +7,8 @@
import noteActions from './note_actions.vue';
import noteBody from './note_body.vue';
import eventHub from '../event_hub';
+ import noteable from '../mixins/noteable';
+ import resolvable from '../mixins/resolvable';
export default {
components: {
@@ -15,6 +17,10 @@
noteActions,
noteBody,
},
+ mixins: [
+ noteable,
+ resolvable,
+ ],
props: {
note: {
type: Object,
@@ -26,6 +32,7 @@
isEditing: false,
isDeleting: false,
isRequesting: false,
+ isResolving: false,
};
},
computed: {
@@ -65,6 +72,7 @@
...mapActions([
'deleteNote',
'updateNote',
+ 'toggleResolveNote',
'scrollToNoteIfNeeded',
]),
editHandler() {
@@ -89,7 +97,7 @@
const data = {
endpoint: this.note.path,
note: {
- target_type: 'issue',
+ target_type: this.noteableType,
target_id: this.note.noteable_id,
note: { note: noteText },
},
@@ -134,7 +142,7 @@
// we need to do this to prevent noteForm inconsistent content warning
// this is something we intentionally do so we need to recover the content
this.note.note = noteText;
- this.$refs.noteBody.$refs.noteForm.note = noteText;
+ this.$refs.noteBody.$refs.noteForm.note.note = noteText;
},
},
};
@@ -171,8 +179,13 @@
:can-delete="note.current_user.can_edit"
:can-report-as-abuse="canReportAsAbuse"
:report-abuse-path="note.report_abuse_path"
+ :resolvable="note.resolvable"
+ :is-resolved="note.resolved"
+ :is-resolving="isResolving"
+ :resolved-by="note.resolved_by"
@handleEdit="editHandler"
@handleDelete="deleteHandler"
+ @handleResolve="resolveHandler"
/>
</div>
<note-body
diff --git a/app/assets/javascripts/notes/components/notes_app.vue b/app/assets/javascripts/notes/components/notes_app.vue
index 92db4830704..74afed5560b 100644
--- a/app/assets/javascripts/notes/components/notes_app.vue
+++ b/app/assets/javascripts/notes/components/notes_app.vue
@@ -11,6 +11,7 @@
import placeholderNote from '../../vue_shared/components/notes/placeholder_note.vue';
import placeholderSystemNote from '../../vue_shared/components/notes/placeholder_system_note.vue';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+ import skeletonLoadingContainer from '../../vue_shared/components/notes/skeleton_note.vue';
export default {
name: 'NotesApp',
@@ -48,7 +49,24 @@
...mapGetters([
'notes',
'getNotesDataByProp',
+ 'discussionCount',
]),
+ noteableType() {
+ // FIXME -- @fatihacet Get this from JSON data.
+ const { ISSUE_NOTEABLE_TYPE, MERGE_REQUEST_NOTEABLE_TYPE } = constants;
+
+ return this.noteableData.merge_params ? MERGE_REQUEST_NOTEABLE_TYPE : ISSUE_NOTEABLE_TYPE;
+ },
+ allNotes() {
+ if (this.isLoading) {
+ const totalNotes = parseInt(this.notesData.totalNotes, 10) || 0;
+
+ return new Array(totalNotes).fill({
+ isSkeletonNote: true,
+ });
+ }
+ return this.notes;
+ },
},
created() {
this.setNotesData(this.notesData);
@@ -67,6 +85,10 @@
this.actionToggleAward({ awardName, noteId });
});
}
+ document.addEventListener('refreshVueNotes', this.fetchNotes);
+ },
+ beforeDestroy() {
+ document.removeEventListener('refreshVueNotes', this.fetchNotes);
},
methods: {
...mapActions({
@@ -81,6 +103,9 @@
setTargetNoteHash: 'setTargetNoteHash',
}),
getComponentName(note) {
+ if (note.isSkeletonNote) {
+ return skeletonLoadingContainer;
+ }
if (note.isPlaceholderNote) {
if (note.placeholderType === constants.SYSTEM_NOTE) {
return placeholderSystemNote;
@@ -109,9 +134,14 @@
});
},
initPolling() {
+ if (this.isPollingInitialized) {
+ return;
+ }
+
this.setLastFetchedAt(this.getNotesDataByProp('lastFetchedAt'));
this.poll();
+ this.isPollingInitialized = true;
},
checkLocationHash() {
const hash = getLocationHash();
@@ -128,25 +158,20 @@
<template>
<div id="notes">
- <div
- v-if="isLoading"
- class="js-loading loading">
- <loading-icon />
- </div>
-
<ul
- v-if="!isLoading"
id="notes-list"
class="notes main-notes-list timeline">
<component
- v-for="note in notes"
+ v-for="note in allNotes"
:is="getComponentName(note)"
:note="getComponentData(note)"
:key="note.id"
/>
</ul>
- <comment-form />
+ <comment-form
+ :noteable-type="noteableType"
+ />
</div>
</template>
diff --git a/app/assets/javascripts/notes/constants.js b/app/assets/javascripts/notes/constants.js
index a6961063c01..f4f407ffd8a 100644
--- a/app/assets/javascripts/notes/constants.js
+++ b/app/assets/javascripts/notes/constants.js
@@ -1,4 +1,5 @@
export const DISCUSSION_NOTE = 'DiscussionNote';
+export const DIFF_NOTE = 'DiffNote';
export const DISCUSSION = 'discussion';
export const NOTE = 'note';
export const SYSTEM_NOTE = 'systemNote';
@@ -8,4 +9,7 @@ export const REOPENED = 'reopened';
export const CLOSED = 'closed';
export const EMOJI_THUMBSUP = 'thumbsup';
export const EMOJI_THUMBSDOWN = 'thumbsdown';
-export const NOTEABLE_TYPE = 'Issue';
+export const ISSUE_NOTEABLE_TYPE = 'issue';
+export const MERGE_REQUEST_NOTEABLE_TYPE = 'merge_request';
+export const UNRESOLVE_NOTE_METHOD_NAME = 'delete';
+export const RESOLVE_NOTE_METHOD_NAME = 'post';
diff --git a/app/assets/javascripts/notes/index.js b/app/assets/javascripts/notes/index.js
index 48e7cfddb63..545bf2c99a7 100644
--- a/app/assets/javascripts/notes/index.js
+++ b/app/assets/javascripts/notes/index.js
@@ -20,17 +20,7 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
return {
noteableData: JSON.parse(notesDataset.noteableData),
currentUserData,
- notesData: {
- lastFetchedAt: notesDataset.lastFetchedAt,
- discussionsPath: notesDataset.discussionsPath,
- newSessionPath: notesDataset.newSessionPath,
- registerPath: notesDataset.registerPath,
- notesPath: notesDataset.notesPath,
- markdownDocsPath: notesDataset.markdownDocsPath,
- quickActionsDocsPath: notesDataset.quickActionsDocsPath,
- closeIssuePath: notesDataset.closeIssuePath,
- reopenIssuePath: notesDataset.reopenIssuePath,
- },
+ notesData: JSON.parse(notesDataset.notesData),
};
},
render(createElement) {
diff --git a/app/assets/javascripts/notes/mixins/autosave.js b/app/assets/javascripts/notes/mixins/autosave.js
index a008171beda..a3d897f2f12 100644
--- a/app/assets/javascripts/notes/mixins/autosave.js
+++ b/app/assets/javascripts/notes/mixins/autosave.js
@@ -1,9 +1,10 @@
import Autosave from '../../autosave';
+import { capitalizeFirstCharacter } from '../../lib/utils/text_utility';
export default {
methods: {
- initAutoSave() {
- this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', 'Issue', this.note.id], 'issue');
+ initAutoSave(noteableType) {
+ this.autosave = new Autosave($(this.$refs.noteForm.$refs.textarea), ['Note', capitalizeFirstCharacter(noteableType), this.note.id]);
},
resetAutoSave() {
this.autosave.reset();
diff --git a/app/assets/javascripts/notes/mixins/noteable.js b/app/assets/javascripts/notes/mixins/noteable.js
new file mode 100644
index 00000000000..0da4ff49f08
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/noteable.js
@@ -0,0 +1,22 @@
+import * as constants from '../constants';
+
+export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ noteableType() {
+ switch (this.note.noteable_type) {
+ case 'MergeRequest':
+ return constants.MERGE_REQUEST_NOTEABLE_TYPE;
+ case 'Issue':
+ return constants.ISSUE_NOTEABLE_TYPE;
+ default:
+ return '';
+ }
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/mixins/resolvable.js b/app/assets/javascripts/notes/mixins/resolvable.js
new file mode 100644
index 00000000000..ab1ae115e52
--- /dev/null
+++ b/app/assets/javascripts/notes/mixins/resolvable.js
@@ -0,0 +1,50 @@
+import Flash from '~/flash';
+import { __ } from '~/locale';
+
+export default {
+ props: {
+ note: {
+ type: Object,
+ required: true,
+ },
+ },
+ computed: {
+ discussionResolved() {
+ const { notes, resolved } = this.note;
+
+ if (notes) { // Decide resolved state using store. Only valid for discussions.
+ return notes.every(note => note.resolved && !note.system);
+ }
+
+ return resolved;
+ },
+ resolveButtonTitle() {
+ if (this.updatedNoteBody) {
+ if (this.discussionResolved) {
+ return __('Comment and unresolve discussion');
+ }
+
+ return __('Comment and resolve discussion');
+ }
+ return this.discussionResolved ? __('Unresolve discussion') : __('Resolve discussion');
+ },
+ },
+ methods: {
+ resolveHandler(resolvedState = false) {
+ this.isResolving = true;
+ const endpoint = this.note.resolve_path || `${this.note.path}/resolve`;
+ const isResolved = this.discussionResolved || resolvedState;
+ const discussion = this.resolveAsThread;
+
+ this.toggleResolveNote({ endpoint, isResolved, discussion })
+ .then(() => {
+ this.isResolving = false;
+ })
+ .catch(() => {
+ this.isResolving = false;
+ const msg = __('Something went wrong while resolving this discussion. Please try again.');
+ Flash(msg, 'alert', this.$el);
+ });
+ },
+ },
+};
diff --git a/app/assets/javascripts/notes/services/notes_service.js b/app/assets/javascripts/notes/services/notes_service.js
index b8e7ffc8c46..4766351dfc5 100644
--- a/app/assets/javascripts/notes/services/notes_service.js
+++ b/app/assets/javascripts/notes/services/notes_service.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import VueResource from 'vue-resource';
+import * as constants from '../constants';
Vue.use(VueResource);
@@ -19,6 +20,12 @@ export default {
createNewNote(endpoint, data) {
return Vue.http.post(endpoint, data, { emulateJSON: true });
},
+ toggleResolveNote(endpoint, isResolved) {
+ const { RESOLVE_NOTE_METHOD_NAME, UNRESOLVE_NOTE_METHOD_NAME } = constants;
+ const method = isResolved ? UNRESOLVE_NOTE_METHOD_NAME : RESOLVE_NOTE_METHOD_NAME;
+
+ return Vue.http[method](endpoint);
+ },
poll(data = {}) {
const { endpoint, lastFetchedAt } = data;
const options = {
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 4c846d69b86..42fc2a131b8 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -61,8 +61,17 @@ export const createNewNote = ({ commit }, { endpoint, data }) => service
export const removePlaceholderNotes = ({ commit }) =>
commit(types.REMOVE_PLACEHOLDER_NOTES);
+export const toggleResolveNote = ({ commit }, { endpoint, isResolved, discussion }) => service
+ .toggleResolveNote(endpoint, isResolved)
+ .then(res => res.json())
+ .then((res) => {
+ const mutationType = discussion ? types.UPDATE_DISCUSSION : types.UPDATE_NOTE;
+
+ commit(mutationType, res);
+ });
+
export const closeIssue = ({ commit, dispatch, state }) => service
- .toggleIssueState(state.notesData.closeIssuePath)
+ .toggleIssueState(state.notesData.closePath)
.then(res => res.json())
.then((data) => {
commit(types.CLOSE_ISSUE);
@@ -70,7 +79,7 @@ export const closeIssue = ({ commit, dispatch, state }) => service
});
export const reopenIssue = ({ commit, dispatch, state }) => service
- .toggleIssueState(state.notesData.reopenIssuePath)
+ .toggleIssueState(state.notesData.reopenPath)
.then(res => res.json())
.then((data) => {
commit(types.REOPEN_ISSUE);
@@ -80,7 +89,7 @@ export const reopenIssue = ({ commit, dispatch, state }) => service
export const emitStateChangedEvent = ({ commit, getters }, data) => {
const event = new CustomEvent('issuable_vue_app:change', { detail: {
data,
- isClosed: getters.issueState === constants.CLOSED,
+ isClosed: getters.openState === constants.CLOSED,
} });
document.dispatchEvent(event);
@@ -174,7 +183,7 @@ const pollSuccessCallBack = (resp, commit, state, getters) => {
resp.notes.forEach((note) => {
if (notesById[note.id]) {
commit(types.UPDATE_NOTE, note);
- } else if (note.type === constants.DISCUSSION_NOTE) {
+ } else if (note.type === constants.DISCUSSION_NOTE || note.type === constants.DIFF_NOTE) {
const discussion = utils.findNoteObjectById(state.notes, note.discussion_id);
if (discussion) {
diff --git a/app/assets/javascripts/notes/stores/getters.js b/app/assets/javascripts/notes/stores/getters.js
index 82024104d73..e6180101c58 100644
--- a/app/assets/javascripts/notes/stores/getters.js
+++ b/app/assets/javascripts/notes/stores/getters.js
@@ -8,7 +8,7 @@ export const getNotesDataByProp = state => prop => state.notesData[prop];
export const getNoteableData = state => state.noteableData;
export const getNoteableDataByProp = state => prop => state.noteableData[prop];
-export const issueState = state => state.noteableData.state;
+export const openState = state => state.noteableData.state;
export const getUserData = state => state.userData || {};
export const getUserDataByProp = state => prop => state.userData && state.userData[prop];
@@ -30,3 +30,37 @@ export const getCurrentUserLastNote = state => _.flatten(
export const getDiscussionLastNote = state => discussion => reverseNotes(discussion.notes)
.find(el => isLastNote(el, state));
+
+export const discussionCount = (state) => {
+ const discussions = state.notes.filter(n => !n.individual_note);
+
+ return discussions.length;
+};
+
+export const unresolvedDiscussions = (state, getters) => {
+ const resolvedMap = getters.resolvedDiscussionsById;
+
+ return state.notes.filter(n => !n.individual_note && !resolvedMap[n.id]);
+};
+
+export const resolvedDiscussionsById = (state) => {
+ const map = {};
+
+ state.notes.forEach((n) => {
+ if (n.notes) {
+ const resolved = n.notes.every(note => note.resolved && !note.system);
+
+ if (resolved) {
+ map[n.id] = n;
+ }
+ }
+ });
+
+ return map;
+};
+
+export const resolvedDiscussionCount = (state, getters) => {
+ const resolvedMap = getters.resolvedDiscussionsById;
+
+ return Object.keys(resolvedMap).length;
+};
diff --git a/app/assets/javascripts/notes/stores/mutation_types.js b/app/assets/javascripts/notes/stores/mutation_types.js
index 6d7c3bbae0f..da1b5a9e51a 100644
--- a/app/assets/javascripts/notes/stores/mutation_types.js
+++ b/app/assets/javascripts/notes/stores/mutation_types.js
@@ -12,6 +12,7 @@ export const SHOW_PLACEHOLDER_NOTE = 'SHOW_PLACEHOLDER_NOTE';
export const TOGGLE_AWARD = 'TOGGLE_AWARD';
export const TOGGLE_DISCUSSION = 'TOGGLE_DISCUSSION';
export const UPDATE_NOTE = 'UPDATE_NOTE';
+export const UPDATE_DISCUSSION = 'UPDATE_DISCUSSION';
// Issue
export const CLOSE_ISSUE = 'CLOSE_ISSUE';
diff --git a/app/assets/javascripts/notes/stores/mutations.js b/app/assets/javascripts/notes/stores/mutations.js
index b3f66578c9a..963b40be3fd 100644
--- a/app/assets/javascripts/notes/stores/mutations.js
+++ b/app/assets/javascripts/notes/stores/mutations.js
@@ -1,22 +1,32 @@
import * as utils from './utils';
import * as types from './mutation_types';
import * as constants from '../constants';
+import { isInMRPage } from '../../lib/utils/common_utils';
export default {
[types.ADD_NEW_NOTE](state, note) {
const { discussion_id, type } = note;
const [exists] = state.notes.filter(n => n.id === note.discussion_id);
+ const isDiscussion = (type === constants.DISCUSSION_NOTE);
if (!exists) {
const noteData = {
expanded: true,
id: discussion_id,
- individual_note: !(type === constants.DISCUSSION_NOTE),
+ individual_note: !isDiscussion,
notes: [note],
reply_id: discussion_id,
};
+ if (isDiscussion && isInMRPage()) {
+ noteData.resolvable = note.resolvable;
+ noteData.resolved = false;
+ noteData.resolve_path = note.resolve_path;
+ noteData.resolve_with_issue_path = note.resolve_with_issue_path;
+ }
+
state.notes.push(noteData);
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
@@ -25,6 +35,7 @@ export default {
if (noteObj) {
noteObj.notes.push(note);
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
}
},
@@ -41,6 +52,8 @@ export default {
state.notes.splice(state.notes.indexOf(noteObj), 1);
}
}
+
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.REMOVE_PLACEHOLDER_NOTES](state) {
@@ -77,15 +90,19 @@ export default {
const notes = [];
notesData.forEach((note) => {
+ const nn = Object.assign({}, note);
+
// To support legacy notes, should be very rare case.
if (note.individual_note && note.notes.length > 1) {
note.notes.forEach((n) => {
- const nn = Object.assign({}, note);
nn.notes = [n]; // override notes array to only have one item to mimick individual_note
notes.push(nn);
});
} else {
- notes.push(note);
+ const oldNote = utils.findNoteObjectById(state.notes, note.id);
+ nn.expanded = oldNote ? oldNote.expanded : note.expanded;
+
+ notes.push(nn);
}
});
@@ -134,6 +151,8 @@ export default {
user: { id, name, username },
});
}
+
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.TOGGLE_DISCUSSION](state, { discussionId }) {
@@ -151,6 +170,24 @@ export default {
const comment = utils.findNoteObjectById(noteObj.notes, note.id);
noteObj.notes.splice(noteObj.notes.indexOf(comment), 1, note);
}
+
+ // document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
+ },
+
+ [types.UPDATE_DISCUSSION](state, noteData) {
+ const note = noteData;
+ let index = 0;
+
+ state.notes.forEach((n, i) => {
+ if (n.id === note.id) {
+ index = i;
+ }
+ });
+
+ note.expanded = true; // override expand flag to prevent collapse
+ state.notes.splice(index, 1, note);
+
+ document.dispatchEvent(new CustomEvent('refreshLegacyNotes'));
},
[types.CLOSE_ISSUE](state) {
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 6074115e855..275263a2aaa 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -28,4 +28,3 @@ export const getQuickActionText = (note) => {
export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
-
diff --git a/app/assets/javascripts/pages/admin/abuse_reports/index.js b/app/assets/javascripts/pages/admin/abuse_reports/index.js
index c0b6e8d4095..d76b1f174fc 100644
--- a/app/assets/javascripts/pages/admin/abuse_reports/index.js
+++ b/app/assets/javascripts/pages/admin/abuse_reports/index.js
@@ -1,3 +1,3 @@
import AbuseReports from './abuse_reports';
-export default () => new AbuseReports();
+document.addEventListener('DOMContentLoaded', () => new AbuseReports());
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
index b68ce5d32d8..f92450cbaa7 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/broadcast_message.js
@@ -3,7 +3,7 @@ import axios from '~/lib/utils/axios_utils';
import flash from '~/flash';
import { __ } from '~/locale';
-export default function initBroadcastMessagesForm() {
+export default () => {
$('input#broadcast_message_color').on('input', function onMessageColorInput() {
const previewColor = $(this).val();
$('div.broadcast-message-preview').css('background-color', previewColor);
@@ -32,4 +32,4 @@ export default function initBroadcastMessagesForm() {
.catch(() => flash(__('An error occurred while rendering preview broadcast message')));
}
}, 250));
-}
+};
diff --git a/app/assets/javascripts/pages/admin/broadcast_messages/index.js b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
index b548c48282a..d6cc6a850eb 100644
--- a/app/assets/javascripts/pages/admin/broadcast_messages/index.js
+++ b/app/assets/javascripts/pages/admin/broadcast_messages/index.js
@@ -1,3 +1,3 @@
import initBroadcastMessagesForm from './broadcast_message';
-export default () => initBroadcastMessagesForm();
+document.addEventListener('DOMContentLoaded', initBroadcastMessagesForm);
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/admin/cohorts/index.js
index 42ef9d38ef7..2d5020dbef4 100644
--- a/app/assets/javascripts/pages/admin/cohorts/index.js
+++ b/app/assets/javascripts/pages/admin/cohorts/index.js
@@ -1,3 +1,3 @@
import initUsagePing from './usage_ping';
-export default () => initUsagePing();
+document.addEventListener('DOMContentLoaded', initUsagePing);
diff --git a/app/assets/javascripts/pages/admin/groups/show/index.js b/app/assets/javascripts/pages/admin/groups/show/index.js
index 5defea104d4..b0cdad627a6 100644
--- a/app/assets/javascripts/pages/admin/groups/show/index.js
+++ b/app/assets/javascripts/pages/admin/groups/show/index.js
@@ -1,3 +1,3 @@
import UsersSelect from '../../../../users_select';
-export default () => new UsersSelect();
+document.addEventListener('DOMContentLoaded', () => new UsersSelect());
diff --git a/app/assets/javascripts/pages/admin/labels/edit/index.js b/app/assets/javascripts/pages/admin/labels/edit/index.js
index d7ec6e47f67..5de1d4d6344 100644
--- a/app/assets/javascripts/pages/admin/labels/edit/index.js
+++ b/app/assets/javascripts/pages/admin/labels/edit/index.js
@@ -1,3 +1,3 @@
import Labels from '../../../../labels';
-export default () => new Labels();
+document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/admin/labels/new/index.js b/app/assets/javascripts/pages/admin/labels/new/index.js
index d7ec6e47f67..5de1d4d6344 100644
--- a/app/assets/javascripts/pages/admin/labels/new/index.js
+++ b/app/assets/javascripts/pages/admin/labels/new/index.js
@@ -1,3 +1,3 @@
import Labels from '../../../../labels';
-export default () => new Labels();
+document.addEventListener('DOMContentLoaded', () => new Labels());
diff --git a/app/assets/javascripts/pages/admin/projects/index.js b/app/assets/javascripts/pages/admin/projects/index.js
index 71e0ddcd7b6..31c96eb87af 100644
--- a/app/assets/javascripts/pages/admin/projects/index.js
+++ b/app/assets/javascripts/pages/admin/projects/index.js
@@ -1,9 +1,9 @@
import ProjectsList from '../../../projects_list';
import NamespaceSelect from '../../../namespace_select';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ProjectsList(); // eslint-disable-line no-new
document.querySelectorAll('.js-namespace-select')
.forEach(dropdown => new NamespaceSelect({ dropdown }));
-};
+});
diff --git a/app/assets/javascripts/pages/dashboard/todos/index/todos.js b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
index b3f6a72fdcb..42f7460ad55 100644
--- a/app/assets/javascripts/pages/dashboard/todos/index/todos.js
+++ b/app/assets/javascripts/pages/dashboard/todos/index/todos.js
@@ -2,9 +2,9 @@
import { visitUrl } from '~/lib/utils/url_utility';
import UsersSelect from '~/users_select';
import { isMetaClick } from '~/lib/utils/common_utils';
-import { __ } from '../../../../locale';
-import flash from '../../../../flash';
-import axios from '../../../../lib/utils/axios_utils';
+import { __ } from '~/locale';
+import flash from '~/flash';
+import axios from '~/lib/utils/axios_utils';
export default class Todos {
constructor() {
diff --git a/app/assets/javascripts/pages/profiles/index.js b/app/assets/javascripts/pages/profiles/index.js
new file mode 100644
index 00000000000..c52ad7bc335
--- /dev/null
+++ b/app/assets/javascripts/pages/profiles/index.js
@@ -0,0 +1,16 @@
+import '~/profile/gl_crop';
+import Profile from '~/profile/profile';
+
+document.addEventListener('DOMContentLoaded', () => {
+ $(document).on('input.ssh_key', '#key_key', function () { // eslint-disable-line func-names
+ const $title = $('#key_title');
+ const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+
+ // Extract the SSH Key title from its comment
+ if (comment && comment.length > 1) {
+ $title.val(comment[1]).change();
+ }
+ });
+
+ new Profile(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/profiles/index/index.js b/app/assets/javascripts/pages/profiles/index/index.js
index 90eed38777a..9bd430f4f11 100644
--- a/app/assets/javascripts/pages/profiles/index/index.js
+++ b/app/assets/javascripts/pages/profiles/index/index.js
@@ -1,7 +1,7 @@
import NotificationsForm from '../../../notifications_form';
import notificationsDropdown from '../../../notifications_dropdown';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new NotificationsForm(); // eslint-disable-line no-new
notificationsDropdown();
-};
+});
diff --git a/app/assets/javascripts/two_factor_auth.js b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
index e3414d9afff..5b2473e0989 100644
--- a/app/assets/javascripts/two_factor_auth.js
+++ b/app/assets/javascripts/pages/profiles/two_factor_auths/index.js
@@ -1,4 +1,4 @@
-import U2FRegister from './u2f/register';
+import U2FRegister from '~/u2f/register';
document.addEventListener('DOMContentLoaded', () => {
const twoFactorNode = document.querySelector('.js-two-factor-auth');
diff --git a/app/assets/javascripts/pages/projects/boards/index.js b/app/assets/javascripts/pages/projects/boards/index.js
index 3aeeedbb45d..5cfe8723204 100644
--- a/app/assets/javascripts/pages/projects/boards/index.js
+++ b/app/assets/javascripts/pages/projects/boards/index.js
@@ -1,7 +1,9 @@
import UsersSelect from '~/users_select';
import ShortcutsNavigation from '~/shortcuts_navigation';
+import initBoards from '~/boards';
document.addEventListener('DOMContentLoaded', () => {
new UsersSelect(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
+ initBoards();
});
diff --git a/app/assets/javascripts/pages/projects/commit/pipelines/index.js b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
index 7889704a324..cd923f13ce8 100644
--- a/app/assets/javascripts/pages/projects/commit/pipelines/index.js
+++ b/app/assets/javascripts/pages/projects/commit/pipelines/index.js
@@ -1,8 +1,10 @@
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
new MiniPipelineGraph({
container: '.js-commit-pipeline-graph',
}).bindEvents();
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
+ initPipelines();
});
diff --git a/app/assets/javascripts/pages/projects/commit/show/index.js b/app/assets/javascripts/pages/projects/commit/show/index.js
index 460a54ab504..1aeed197385 100644
--- a/app/assets/javascripts/pages/projects/commit/show/index.js
+++ b/app/assets/javascripts/pages/projects/commit/show/index.js
@@ -5,6 +5,7 @@ import ShortcutsNavigation from '~/shortcuts_navigation';
import MiniPipelineGraph from '~/mini_pipeline_graph_dropdown';
import initNotes from '~/init_notes';
import initChangesDropdown from '~/init_changes_dropdown';
+import initDiffNotes from '~/diff_notes/diff_notes_bundle';
import { fetchCommitMergeRequests } from '~/commit_merge_requests';
document.addEventListener('DOMContentLoaded', () => {
@@ -19,4 +20,5 @@ document.addEventListener('DOMContentLoaded', () => {
initChangesDropdown(document.querySelector('.navbar-gitlab').offsetHeight - stickyBarPaddingTop);
$('.commit-info.branches').load(document.querySelector('.js-commit-box').dataset.commitPath);
fetchCommitMergeRequests();
+ initDiffNotes();
});
diff --git a/app/assets/javascripts/pages/projects/compare/index.js b/app/assets/javascripts/pages/projects/compare/index.js
index 890062eeee6..d1c78bd61db 100644
--- a/app/assets/javascripts/pages/projects/compare/index.js
+++ b/app/assets/javascripts/pages/projects/compare/index.js
@@ -1,5 +1,3 @@
import initCompareAutocomplete from '~/compare_autocomplete';
-export default () => {
- initCompareAutocomplete();
-};
+document.addEventListener('DOMContentLoaded', initCompareAutocomplete);
diff --git a/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
new file mode 100644
index 00000000000..df58e9dd072
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/cycle_analytics/show/index.js
@@ -0,0 +1,3 @@
+import initCycleAnalytics from '~/cycle_analytics/cycle_analytics_bundle';
+
+document.addEventListener('DOMContentLoaded', initCycleAnalytics);
diff --git a/app/assets/javascripts/pages/projects/environments/folder/index.js b/app/assets/javascripts/pages/projects/environments/folder/index.js
new file mode 100644
index 00000000000..5feaf944038
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/folder/index.js
@@ -0,0 +1,3 @@
+import initEnvironmentsFolderBundle from '~/environments/folder/environments_folder_bundle';
+
+document.addEventListener('DOMContentLoaded', initEnvironmentsFolderBundle);
diff --git a/app/assets/javascripts/pages/projects/environments/index.js b/app/assets/javascripts/pages/projects/environments/index.js
new file mode 100644
index 00000000000..ace8af00ece
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/index.js
@@ -0,0 +1,3 @@
+import initEnviroments from '~/environments/';
+
+document.addEventListener('DOMContentLoaded', initEnviroments);
diff --git a/app/assets/javascripts/pages/projects/environments/terminal/index.js b/app/assets/javascripts/pages/projects/environments/terminal/index.js
new file mode 100644
index 00000000000..7129e24cee1
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/environments/terminal/index.js
@@ -0,0 +1,3 @@
+import initTerminal from '~/terminal/';
+
+document.addEventListener('DOMContentLoaded', initTerminal);
diff --git a/app/assets/javascripts/pages/projects/index.js b/app/assets/javascripts/pages/projects/index.js
index 9b1d52692a3..de1e13de7e9 100644
--- a/app/assets/javascripts/pages/projects/index.js
+++ b/app/assets/javascripts/pages/projects/index.js
@@ -1,7 +1,7 @@
import Project from './project';
import ShortcutsNavigation from '../../shortcuts_navigation';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new Project(); // eslint-disable-line no-new
new ShortcutsNavigation(); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/projects/issues/show.js b/app/assets/javascripts/pages/projects/issues/show.js
new file mode 100644
index 00000000000..500fbd27340
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/issues/show.js
@@ -0,0 +1,13 @@
+import initIssuableSidebar from '~/init_issuable_sidebar';
+import Issue from '~/issue';
+import ShortcutsIssuable from '~/shortcuts_issuable';
+import ZenMode from '~/zen_mode';
+import '~/notes/index';
+import '~/issue_show/index';
+
+export default function () {
+ new Issue(); // eslint-disable-line no-new
+ new ShortcutsIssuable(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+ initIssuableSidebar();
+}
diff --git a/app/assets/javascripts/pages/projects/issues/show/index.js b/app/assets/javascripts/pages/projects/issues/show/index.js
index db064e3f801..7968dfd7a12 100644
--- a/app/assets/javascripts/pages/projects/issues/show/index.js
+++ b/app/assets/javascripts/pages/projects/issues/show/index.js
@@ -1,12 +1,7 @@
-import initIssuableSidebar from '~/init_issuable_sidebar';
-import Issue from '~/issue';
-import ShortcutsIssuable from '~/shortcuts_issuable';
-import ZenMode from '~/zen_mode';
-import '~/notes/index';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initShow from '../show';
document.addEventListener('DOMContentLoaded', () => {
- new Issue(); // eslint-disable-line no-new
- new ShortcutsIssuable(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
- initIssuableSidebar();
+ initShow();
+ initSidebarBundle();
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
new file mode 100644
index 00000000000..28641104c58
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/conflicts/index.js
@@ -0,0 +1,7 @@
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initMergeConflicts from '~/merge_conflicts/merge_conflicts_bundle';
+
+document.addEventListener('DOMContentLoaded', () => {
+ initSidebarBundle();
+ initMergeConflicts();
+});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
index 1d5aec4001d..6c9afddefac 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/creations/new/index.js
@@ -1,5 +1,6 @@
import Compare from '~/compare';
import MergeRequest from '~/merge_request';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
document.addEventListener('DOMContentLoaded', () => {
const mrNewCompareNode = document.querySelector('.js-merge-request-new-compare');
@@ -14,5 +15,6 @@ document.addEventListener('DOMContentLoaded', () => {
new MergeRequest({ // eslint-disable-line no-new
action: mrNewSubmitNode.dataset.mrSubmitAction,
});
+ initPipelines();
}
});
diff --git a/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
new file mode 100644
index 00000000000..28d8761b502
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/merge_requests/init_merge_request_show.js
@@ -0,0 +1,32 @@
+import MergeRequest from '~/merge_request';
+import ZenMode from '~/zen_mode';
+import initNotes from '~/init_notes';
+import initIssuableSidebar from '~/init_issuable_sidebar';
+import initDiffNotes from '~/diff_notes/diff_notes_bundle';
+import ShortcutsIssuable from '~/shortcuts_issuable';
+import Diff from '~/diff';
+import { handleLocationHash } from '~/lib/utils/common_utils';
+import howToMerge from '~/how_to_merge';
+import initPipelines from '~/commit/pipelines/pipelines_bundle';
+import initWidget from '../../../vue_merge_request_widget';
+
+export default function () {
+ new Diff(); // eslint-disable-line no-new
+ new ZenMode(); // eslint-disable-line no-new
+
+ initIssuableSidebar();
+ initNotes();
+ initDiffNotes();
+ initPipelines();
+
+ const mrShowNode = document.querySelector('.merge-request');
+
+ window.mergeRequest = new MergeRequest({
+ action: mrShowNode.dataset.mrAction,
+ });
+
+ new ShortcutsIssuable(true); // eslint-disable-line no-new
+ handleLocationHash();
+ howToMerge();
+ initWidget();
+}
diff --git a/app/assets/javascripts/pages/projects/merge_requests/show/index.js b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
index 07f3e579c97..e5b2827b50c 100644
--- a/app/assets/javascripts/pages/projects/merge_requests/show/index.js
+++ b/app/assets/javascripts/pages/projects/merge_requests/show/index.js
@@ -1,28 +1,13 @@
-import MergeRequest from '~/merge_request';
-import ZenMode from '~/zen_mode';
-import initNotes from '~/init_notes';
-import initIssuableSidebar from '~/init_issuable_sidebar';
-import initDiffNotes from '~/diff_notes/diff_notes_bundle';
-import ShortcutsIssuable from '~/shortcuts_issuable';
-import Diff from '~/diff';
-import { handleLocationHash } from '~/lib/utils/common_utils';
-import howToMerge from '~/how_to_merge';
+import { hasVueMRDiscussionsCookie } from '~/lib/utils/common_utils';
+import initMrNotes from '~/mr_notes';
+import initSidebarBundle from '~/sidebar/sidebar_bundle';
+import initShow from '../init_merge_request_show';
document.addEventListener('DOMContentLoaded', () => {
- new Diff(); // eslint-disable-line no-new
- new ZenMode(); // eslint-disable-line no-new
+ initShow();
+ initSidebarBundle();
- initIssuableSidebar();
- initNotes();
- initDiffNotes();
-
- const mrShowNode = document.querySelector('.merge-request');
-
- window.mergeRequest = new MergeRequest({
- action: mrShowNode.dataset.mrAction,
- });
-
- new ShortcutsIssuable(true); // eslint-disable-line no-new
- handleLocationHash();
- howToMerge();
+ if (hasVueMRDiscussionsCookie()) {
+ initMrNotes();
+ }
});
diff --git a/app/assets/javascripts/network/network.js b/app/assets/javascripts/pages/projects/network/network.js
index a3fd22aff2a..7354243e4c8 100644
--- a/app/assets/javascripts/network/network.js
+++ b/app/assets/javascripts/pages/projects/network/network.js
@@ -1,6 +1,6 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-var, quotes, quote-props, prefer-template, comma-dangle, max-len */
-import BranchGraph from './branch_graph';
+import BranchGraph from '../../../network/branch_graph';
export default (function() {
function Network(opts) {
diff --git a/app/assets/javascripts/pages/projects/network/show/index.js b/app/assets/javascripts/pages/projects/network/show/index.js
new file mode 100644
index 00000000000..e7dfd2d0128
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/network/show/index.js
@@ -0,0 +1,16 @@
+import ShortcutsNetwork from '../../../../shortcuts_network';
+import Network from '../network';
+
+document.addEventListener('DOMContentLoaded', () => {
+ if (!$('.network-graph').length) return;
+
+ const networkGraph = new Network({
+ url: $('.network-graph').attr('data-url'),
+ commit_url: $('.network-graph').attr('data-commit-url'),
+ ref: $('.network-graph').attr('data-ref'),
+ commit_id: $('.network-graph').attr('data-commit-id'),
+ });
+
+ // eslint-disable-next-line no-new
+ new ShortcutsNetwork(networkGraph.branch_graph);
+});
diff --git a/app/assets/javascripts/pages/projects/new/index.js b/app/assets/javascripts/pages/projects/new/index.js
index 71c49deb9d0..ea6fd961393 100644
--- a/app/assets/javascripts/pages/projects/new/index.js
+++ b/app/assets/javascripts/pages/projects/new/index.js
@@ -2,8 +2,8 @@ import ProjectNew from '../shared/project_new';
import initProjectVisibilitySelector from '../../../project_visibility';
import initProjectNew from '../../../projects/project_new';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new ProjectNew(); // eslint-disable-line no-new
initProjectVisibilitySelector();
initProjectNew.bindEvents();
-};
+});
diff --git a/app/assets/javascripts/pages/projects/pipelines/builds/index.js b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
index fbe9824c34b..7a57e417b41 100644
--- a/app/assets/javascripts/pages/projects/pipelines/builds/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/builds/index.js
@@ -1,3 +1,7 @@
+import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
-document.addEventListener('DOMContentLoaded', initPipelines);
+document.addEventListener('DOMContentLoaded', () => {
+ initPipelines();
+ initPipelineDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/pipelines/index/index.js b/app/assets/javascripts/pages/projects/pipelines/index/index.js
new file mode 100644
index 00000000000..a84e2790680
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/pipelines/index/index.js
@@ -0,0 +1,40 @@
+import Vue from 'vue';
+import PipelinesStore from '../../../../pipelines/stores/pipelines_store';
+import pipelinesComponent from '../../../../pipelines/components/pipelines.vue';
+import Translate from '../../../../vue_shared/translate';
+import { convertPermissionToBoolean } from '../../../../lib/utils/common_utils';
+
+Vue.use(Translate);
+
+document.addEventListener('DOMContentLoaded', () => new Vue({
+ el: '#pipelines-list-vue',
+ components: {
+ pipelinesComponent,
+ },
+ data() {
+ return {
+ store: new PipelinesStore(),
+ };
+ },
+ created() {
+ this.dataset = document.querySelector(this.$options.el).dataset;
+ },
+ render(createElement) {
+ return createElement('pipelines-component', {
+ props: {
+ store: this.store,
+ endpoint: this.dataset.endpoint,
+ helpPagePath: this.dataset.helpPagePath,
+ emptyStateSvgPath: this.dataset.emptyStateSvgPath,
+ errorStateSvgPath: this.dataset.errorStateSvgPath,
+ noPipelinesSvgPath: this.dataset.noPipelinesSvgPath,
+ autoDevopsPath: this.dataset.helpAutoDevopsPath,
+ newPipelinePath: this.dataset.newPipelinePath,
+ canCreatePipeline: convertPermissionToBoolean(this.dataset.canCreatePipeline),
+ hasGitlabCi: convertPermissionToBoolean(this.dataset.hasGitlabCi),
+ ciLintPath: this.dataset.ciLintPath,
+ resetCachePath: this.dataset.resetCachePath,
+ },
+ });
+ },
+}));
diff --git a/app/assets/javascripts/pages/projects/pipelines/show/index.js b/app/assets/javascripts/pages/projects/pipelines/show/index.js
index fbe9824c34b..7a57e417b41 100644
--- a/app/assets/javascripts/pages/projects/pipelines/show/index.js
+++ b/app/assets/javascripts/pages/projects/pipelines/show/index.js
@@ -1,3 +1,7 @@
+import initPipelineDetails from '~/pipelines/pipeline_details_bundle';
import initPipelines from '../init_pipelines';
-document.addEventListener('DOMContentLoaded', initPipelines);
+document.addEventListener('DOMContentLoaded', () => {
+ initPipelines();
+ initPipelineDetails();
+});
diff --git a/app/assets/javascripts/pages/projects/registry/repositories/index.js b/app/assets/javascripts/pages/projects/registry/repositories/index.js
new file mode 100644
index 00000000000..35564754ee0
--- /dev/null
+++ b/app/assets/javascripts/pages/projects/registry/repositories/index.js
@@ -0,0 +1,3 @@
+import initRegistryImages from '~/registry/index';
+
+document.addEventListener('DOMContentLoaded', initRegistryImages);
diff --git a/app/assets/javascripts/pages/projects/settings/repository/show/index.js b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
index d88527351c1..788d86d1192 100644
--- a/app/assets/javascripts/pages/projects/settings/repository/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/repository/show/index.js
@@ -1,3 +1,17 @@
+/* eslint-disable no-new */
+
+import ProtectedTagCreate from '~/protected_tags/protected_tag_create';
+import ProtectedTagEditList from '~/protected_tags/protected_tag_edit_list';
import initSettingsPanels from '~/settings_panels';
+import initDeployKeys from '~/deploy_keys';
+import ProtectedBranchCreate from '~/protected_branches/protected_branch_create';
+import ProtectedBranchEditList from '~/protected_branches/protected_branch_edit_list';
-document.addEventListener('DOMContentLoaded', initSettingsPanels);
+document.addEventListener('DOMContentLoaded', () => {
+ new ProtectedTagCreate();
+ new ProtectedTagEditList();
+ initDeployKeys();
+ initSettingsPanels();
+ new ProtectedBranchCreate(); // eslint-disable-line no-new
+ new ProtectedBranchEditList(); // eslint-disable-line no-new
+});
diff --git a/app/assets/javascripts/pages/projects/snippets/edit/index.js b/app/assets/javascripts/pages/projects/snippets/edit/index.js
index caf9ee9b398..c15f798b630 100644
--- a/app/assets/javascripts/pages/projects/snippets/edit/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/edit/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form';
-document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form')));
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ initForm($('.snippet-form'));
+});
diff --git a/app/assets/javascripts/pages/projects/snippets/new/index.js b/app/assets/javascripts/pages/projects/snippets/new/index.js
index caf9ee9b398..c15f798b630 100644
--- a/app/assets/javascripts/pages/projects/snippets/new/index.js
+++ b/app/assets/javascripts/pages/projects/snippets/new/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import initForm from '~/pages/projects/init_form';
-document.addEventListener('DOMContentLoaded', () => initForm($('.snippet-form')));
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ initForm($('.snippet-form'));
+});
diff --git a/app/assets/javascripts/pages/projects/wikis/index.js b/app/assets/javascripts/pages/projects/wikis/index.js
index eb14c7a0e78..b9f8707fd6e 100644
--- a/app/assets/javascripts/pages/projects/wikis/index.js
+++ b/app/assets/javascripts/pages/projects/wikis/index.js
@@ -3,9 +3,9 @@ import ShortcutsWiki from '../../../shortcuts_wiki';
import ZenMode from '../../../zen_mode';
import GLForm from '../../../gl_form';
-export default () => {
+document.addEventListener('DOMContentLoaded', () => {
new Wikis(); // eslint-disable-line no-new
new ShortcutsWiki(); // eslint-disable-line no-new
new ZenMode(); // eslint-disable-line no-new
new GLForm($('.wiki-form'), true); // eslint-disable-line no-new
-};
+});
diff --git a/app/assets/javascripts/pages/search/init_filtered_search.js b/app/assets/javascripts/pages/search/init_filtered_search.js
index de8d4168d71..7fdf4ee0bf3 100644
--- a/app/assets/javascripts/pages/search/init_filtered_search.js
+++ b/app/assets/javascripts/pages/search/init_filtered_search.js
@@ -1,9 +1,23 @@
import FilteredSearchManager from '~/filtered_search/filtered_search_manager';
-export default ({ page }) => {
+export default ({
+ page,
+ filteredSearchTokenKeys,
+ isGroup,
+ isGroupAncestor,
+ isGroupDecendent,
+ stateFiltersSelector,
+}) => {
const filteredSearchEnabled = FilteredSearchManager && document.querySelector('.filtered-search');
if (filteredSearchEnabled) {
- const filteredSearchManager = new FilteredSearchManager({ page });
+ const filteredSearchManager = new FilteredSearchManager({
+ page,
+ isGroup,
+ isGroupAncestor,
+ isGroupDecendent,
+ filteredSearchTokenKeys,
+ stateFiltersSelector,
+ });
filteredSearchManager.setup();
}
};
diff --git a/app/assets/javascripts/pages/snippets/edit/index.js b/app/assets/javascripts/pages/snippets/edit/index.js
index 2ee38b64ca1..d86e1632ae5 100644
--- a/app/assets/javascripts/pages/snippets/edit/index.js
+++ b/app/assets/javascripts/pages/snippets/edit/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import form from '../form';
-document.addEventListener('DOMContentLoaded', form);
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ form();
+});
diff --git a/app/assets/javascripts/pages/snippets/new/index.js b/app/assets/javascripts/pages/snippets/new/index.js
index 2ee38b64ca1..d86e1632ae5 100644
--- a/app/assets/javascripts/pages/snippets/new/index.js
+++ b/app/assets/javascripts/pages/snippets/new/index.js
@@ -1,3 +1,7 @@
+import initSnippet from '~/snippet/snippet_bundle';
import form from '../form';
-document.addEventListener('DOMContentLoaded', form);
+document.addEventListener('DOMContentLoaded', () => {
+ initSnippet();
+ form();
+});
diff --git a/app/assets/javascripts/pipelines/components/blank_state.vue b/app/assets/javascripts/pipelines/components/blank_state.vue
new file mode 100644
index 00000000000..8d3d6223d7b
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/blank_state.vue
@@ -0,0 +1,32 @@
+<script>
+ export default {
+ name: 'PipelinesSvgState',
+ props: {
+ svgPath: {
+ type: String,
+ required: true,
+ },
+
+ message: {
+ type: String,
+ required: true,
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="row empty-state">
+ <div class="col-xs-12">
+ <div class="svg-content">
+ <img :src="svgPath" />
+ </div>
+ </div>
+
+ <div class="col-xs-12 text-center">
+ <div class="text-content">
+ <h4>{{ message }}</h4>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/empty_state.vue b/app/assets/javascripts/pipelines/components/empty_state.vue
index dfaa2574091..10ac8c08bed 100644
--- a/app/assets/javascripts/pipelines/components/empty_state.vue
+++ b/app/assets/javascripts/pipelines/components/empty_state.vue
@@ -1,5 +1,6 @@
<script>
export default {
+ name: 'PipelinesEmptyState',
props: {
helpPagePath: {
type: String,
@@ -9,6 +10,10 @@
type: String,
required: true,
},
+ canSetCi: {
+ type: Boolean,
+ required: true,
+ },
},
};
</script>
@@ -22,22 +27,36 @@
<div class="col-xs-12">
<div class="text-content">
- <h4 class="text-center">
- {{ s__("Pipelines|Build with confidence") }}
- </h4>
- <p>
- {{ s__(`Pipelines|Continous Integration can help
-catch bugs by running your tests automatically,
-while Continuous Deployment can help you deliver code to your product environment.`) }}
+
+ <template v-if="canSetCi">
+ <h4 class="text-center">
+ {{ s__('Pipelines|Build with confidence') }}
+ </h4>
+
+ <p>
+ {{ s__(`Pipelines|Continous Integration can help
+ catch bugs by running your tests automatically,
+ while Continuous Deployment can help you deliver
+ code to your product environment.`) }}
+ </p>
+
+ <div class="text-center">
+ <a
+ :href="helpPagePath"
+ class="btn btn-primary js-get-started-pipelines"
+ >
+ {{ s__('Pipelines|Get started with Pipelines') }}
+ </a>
+ </div>
+ </template>
+
+ <p
+ v-else
+ class="text-center"
+ >
+ {{ s__('Pipelines|This project is not currently set up to run pipelines.') }}
</p>
- <div class="text-center">
- <a
- :href="helpPagePath"
- class="btn btn-info"
- >
- {{ s__("Pipelines|Get started with Pipelines") }}
- </a>
- </div>
+
</div>
</div>
</div>
diff --git a/app/assets/javascripts/pipelines/components/error_state.vue b/app/assets/javascripts/pipelines/components/error_state.vue
deleted file mode 100644
index 012853b201d..00000000000
--- a/app/assets/javascripts/pipelines/components/error_state.vue
+++ /dev/null
@@ -1,26 +0,0 @@
-<script>
-export default {
- props: {
- errorStateSvgPath: {
- type: String,
- required: true,
- },
- },
-};
-</script>
-
-<template>
- <div class="row empty-state js-pipelines-error-state">
- <div class="col-xs-12">
- <div class="svg-content">
- <img :src="errorStateSvgPath"/>
- </div>
- </div>
-
- <div class="col-xs-12 text-center">
- <div class="text-content">
- <h4>The API failed to fetch the pipelines.</h4>
- </div>
- </div>
- </div>
-</template>
diff --git a/app/assets/javascripts/pipelines/components/nav_controls.vue b/app/assets/javascripts/pipelines/components/nav_controls.vue
index f31a91c3403..383ab51fe56 100644
--- a/app/assets/javascripts/pipelines/components/nav_controls.vue
+++ b/app/assets/javascripts/pipelines/components/nav_controls.vue
@@ -1,67 +1,52 @@
<script>
-export default {
- name: 'PipelineNavControls',
- props: {
- newPipelinePath: {
- type: String,
- required: true,
+ export default {
+ name: 'PipelineNavControls',
+ props: {
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
-
- hasCiEnabled: {
- type: Boolean,
- required: true,
- },
-
- helpPagePath: {
- type: String,
- required: true,
- },
-
- resetCachePath: {
- type: String,
- required: true,
- },
-
- ciLintPath: {
- type: String,
- required: true,
- },
-
- canCreatePipeline: {
- type: Boolean,
- required: true,
- },
- },
-};
+ };
</script>
<template>
<div class="nav-controls">
<a
- v-if="canCreatePipeline"
+ v-if="newPipelinePath"
:href="newPipelinePath"
- class="btn btn-create">
- Run Pipeline
- </a>
-
- <a
- v-if="!hasCiEnabled"
- :href="helpPagePath"
- class="btn btn-info">
- Get started with Pipelines
+ class="btn btn-create js-run-pipeline"
+ >
+ {{ s__('Pipelines|Run Pipeline') }}
</a>
<a
+ v-if="resetCachePath"
data-method="post"
- rel="nofollow"
:href="resetCachePath"
- class="btn btn-default">
- Clear runner caches
+ class="btn btn-default js-clear-cache"
+ >
+ {{ s__('Pipelines|Clear Runner Caches') }}
</a>
<a
+ v-if="ciLintPath"
:href="ciLintPath"
- class="btn btn-default">
- CI Lint
+ class="btn btn-default js-ci-lint"
+ >
+ {{ s__('Pipelines|CI Lint') }}
</a>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 90930d5ff44..6e5ee68eeb1 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -1,12 +1,12 @@
<script>
import _ from 'underscore';
+ import { __, sprintf, s__ } from '../../locale';
import PipelinesService from '../services/pipelines_service';
import pipelinesMixin from '../mixins/pipelines';
- import tablePagination from '../../vue_shared/components/table_pagination.vue';
- import navigationTabs from '../../vue_shared/components/navigation_tabs.vue';
- import navigationControls from './nav_controls.vue';
+ import TablePagination from '../../vue_shared/components/table_pagination.vue';
+ import NavigationTabs from '../../vue_shared/components/navigation_tabs.vue';
+ import NavigationControls from './nav_controls.vue';
import {
- convertPermissionToBoolean,
getParameterByName,
parseQueryStringIntoObject,
} from '../../lib/utils/common_utils';
@@ -14,9 +14,9 @@
export default {
components: {
- tablePagination,
- navigationTabs,
- navigationControls,
+ TablePagination,
+ NavigationTabs,
+ NavigationControls,
},
mixins: [
pipelinesMixin,
@@ -36,111 +36,186 @@
required: false,
default: 'root',
},
+ endpoint: {
+ type: String,
+ required: true,
+ },
+ helpPagePath: {
+ type: String,
+ required: true,
+ },
+ emptyStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ errorStateSvgPath: {
+ type: String,
+ required: true,
+ },
+ noPipelinesSvgPath: {
+ type: String,
+ required: true,
+ },
+ autoDevopsPath: {
+ type: String,
+ required: true,
+ },
+ hasGitlabCi: {
+ type: Boolean,
+ required: true,
+ },
+ canCreatePipeline: {
+ type: Boolean,
+ required: true,
+ },
+ ciLintPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ resetCachePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ newPipelinePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
},
data() {
- const pipelinesData = document.querySelector('#pipelines-list-vue').dataset;
-
return {
- endpoint: pipelinesData.endpoint,
- helpPagePath: pipelinesData.helpPagePath,
- emptyStateSvgPath: pipelinesData.emptyStateSvgPath,
- errorStateSvgPath: pipelinesData.errorStateSvgPath,
- autoDevopsPath: pipelinesData.helpAutoDevopsPath,
- newPipelinePath: pipelinesData.newPipelinePath,
- canCreatePipeline: pipelinesData.canCreatePipeline,
- hasCi: pipelinesData.hasCi,
- ciLintPath: pipelinesData.ciLintPath,
- resetCachePath: pipelinesData.resetCachePath,
+ // Start with loading state to avoid a glitch when the empty state will be rendered
+ isLoading: true,
state: this.store.state,
scope: getParameterByName('scope') || 'all',
page: getParameterByName('page') || '1',
requestData: {},
};
},
- computed: {
- canCreatePipelineParsed() {
- return convertPermissionToBoolean(this.canCreatePipeline);
- },
+ stateMap: {
+ // with tabs
+ loading: 'loading',
+ tableList: 'tableList',
+ error: 'error',
+ emptyTab: 'emptyTab',
+ // without tabs
+ emptyState: 'emptyState',
+ },
+ scopes: {
+ all: 'all',
+ pending: 'pending',
+ running: 'running',
+ finished: 'finished',
+ branches: 'branches',
+ tags: 'tags',
+ },
+ computed: {
/**
- * The empty state should only be rendered when the request is made to fetch all pipelines
- * and none is returned.
- *
- * @return {Boolean}
- */
- shouldRenderEmptyState() {
- return !this.isLoading &&
- !this.hasError &&
- this.hasMadeRequest &&
- !this.state.pipelines.length &&
- (this.scope === 'all' || this.scope === null);
+ * `hasGitlabCi` handles both internal and external CI.
+ * The order on which the checks are made in this method is
+ * important to guarantee we handle all the corner cases.
+ */
+ stateToRender() {
+ const { stateMap } = this.$options;
+
+ if (this.isLoading) {
+ return stateMap.loading;
+ }
+
+ if (this.hasError) {
+ return stateMap.error;
+ }
+
+ if (this.state.pipelines.length) {
+ return stateMap.tableList;
+ }
+
+ if ((this.scope !== 'all' && this.scope !== null) || this.hasGitlabCi) {
+ return stateMap.emptyTab;
+ }
+
+ return stateMap.emptyState;
},
/**
- * When a specific scope does not have pipelines we render a message.
- *
- * @return {Boolean}
+ * Tabs are rendered in all states except empty state.
+ * They are not rendered before the first request to avoid a flicker on first load.
*/
- shouldRenderNoPipelinesMessage() {
- return !this.isLoading &&
- !this.hasError &&
- !this.state.pipelines.length &&
- this.scope !== 'all' &&
- this.scope !== null;
+ shouldRenderTabs() {
+ const { stateMap } = this.$options;
+ return this.hasMadeRequest &&
+ [
+ stateMap.loading,
+ stateMap.tableList,
+ stateMap.error,
+ stateMap.emptyTab,
+ ].includes(this.stateToRender);
},
- shouldRenderTable() {
- return !this.hasError &&
- !this.isLoading && this.state.pipelines.length;
+ shouldRenderButtons() {
+ return (this.newPipelinePath ||
+ this.resetCachePath ||
+ this.ciLintPath) && this.shouldRenderTabs;
},
- /**
- * Pagination should only be rendered when there is more than one page.
- *
- * @return {Boolean}
- */
+
shouldRenderPagination() {
return !this.isLoading &&
this.state.pipelines.length &&
this.state.pageInfo.total > this.state.pageInfo.perPage;
},
- hasCiEnabled() {
- return this.hasCi !== undefined;
+
+ emptyTabMessage() {
+ const { scopes } = this.$options;
+ const possibleScopes = [scopes.pending, scopes.running, scopes.finished];
+
+ if (possibleScopes.includes(this.scope)) {
+ return sprintf(s__('Pipelines|There are currently no %{scope} pipelines.'), {
+ scope: this.scope,
+ });
+ }
+
+ return s__('Pipelines|There are currently no pipelines.');
},
tabs() {
const { count } = this.state;
+ const { scopes } = this.$options;
+
return [
{
- name: 'All',
- scope: 'all',
+ name: __('All'),
+ scope: scopes.all,
count: count.all,
isActive: this.scope === 'all',
},
{
- name: 'Pending',
- scope: 'pending',
+ name: __('Pending'),
+ scope: scopes.pending,
count: count.pending,
isActive: this.scope === 'pending',
},
{
- name: 'Running',
- scope: 'running',
+ name: __('Running'),
+ scope: scopes.running,
count: count.running,
isActive: this.scope === 'running',
},
{
- name: 'Finished',
- scope: 'finished',
+ name: __('Finished'),
+ scope: scopes.finished,
count: count.finished,
isActive: this.scope === 'finished',
},
{
- name: 'Branches',
- scope: 'branches',
+ name: __('Branches'),
+ scope: scopes.branches,
isActive: this.scope === 'branches',
},
{
- name: 'Tags',
- scope: 'tags',
+ name: __('Tags'),
+ scope: scopes.tags,
isActive: this.scope === 'tags',
},
];
@@ -187,7 +262,7 @@
this.errorCallback();
// restart polling
- this.poll.restart();
+ this.poll.restart({ data: this.requestData });
});
},
},
@@ -197,69 +272,70 @@
<div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
- v-if="!shouldRenderEmptyState"
+ v-if="shouldRenderTabs || shouldRenderButtons"
>
<div class="fade-left">
<i
class="fa fa-angle-left"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</div>
<div class="fade-right">
<i
class="fa fa-angle-right"
- aria-hidden="true">
+ aria-hidden="true"
+ >
</i>
</div>
<navigation-tabs
+ v-if="shouldRenderTabs"
:tabs="tabs"
@onChangeTab="onChangeTab"
scope="pipelines"
/>
<navigation-controls
+ v-if="shouldRenderButtons"
:new-pipeline-path="newPipelinePath"
- :has-ci-enabled="hasCiEnabled"
- :help-page-path="helpPagePath"
:reset-cache-path="resetCachePath"
:ci-lint-path="ciLintPath"
- :can-create-pipeline="canCreatePipelineParsed "
/>
</div>
<div class="content-list pipelines">
<loading-icon
- label="Loading Pipelines"
+ v-if="stateToRender === $options.stateMap.loading"
+ :label="s__('Pipelines|Loading Pipelines')"
size="3"
- v-if="isLoading"
class="prepend-top-20"
/>
<empty-state
- v-if="shouldRenderEmptyState"
+ v-else-if="stateToRender === $options.stateMap.emptyState"
:help-page-path="helpPagePath"
:empty-state-svg-path="emptyStateSvgPath"
+ :can-set-ci="canCreatePipeline"
/>
- <error-state
- v-if="shouldRenderErrorState"
- :error-state-svg-path="errorStateSvgPath"
+ <svg-blank-state
+ v-else-if="stateToRender === $options.stateMap.error"
+ :svg-path="errorStateSvgPath"
+ :message="s__(`Pipelines|There was an error fetching the pipelines.
+ Try again in a few moments or contact your support team.`)"
/>
- <div
- class="blank-state-row"
- v-if="shouldRenderNoPipelinesMessage"
- >
- <div class="blank-state-center">
- <h2 class="blank-state-title js-blank-state-title">No pipelines to show.</h2>
- </div>
- </div>
+ <svg-blank-state
+ v-else-if="stateToRender === $options.stateMap.emptyTab"
+ :svg-path="noPipelinesSvgPath"
+ :message="emptyTabMessage"
+ />
<div
class="table-holder"
- v-if="shouldRenderTable"
+ v-else-if="stateToRender === $options.stateMap.tableList"
>
<pipelines-table-component
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index 2ba59051773..4cbd67e0372 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -316,7 +316,7 @@
v-if="pipeline.flags.cancelable"
:endpoint="pipeline.cancel_path"
css-class="js-pipelines-cancel-button btn-remove"
- title="Cancel"
+ title="Stop"
icon="close"
:pipeline-id="pipeline.id"
data-toggle="modal"
diff --git a/app/assets/javascripts/pipelines/mixins/pipelines.js b/app/assets/javascripts/pipelines/mixins/pipelines.js
index 50bdf80c3e3..9fcc07abee5 100644
--- a/app/assets/javascripts/pipelines/mixins/pipelines.js
+++ b/app/assets/javascripts/pipelines/mixins/pipelines.js
@@ -1,23 +1,19 @@
import Visibility from 'visibilityjs';
+import { __ } from '../../locale';
import Flash from '../../flash';
import Poll from '../../lib/utils/poll';
-import emptyState from '../components/empty_state.vue';
-import errorState from '../components/error_state.vue';
-import loadingIcon from '../../vue_shared/components/loading_icon.vue';
-import pipelinesTableComponent from '../components/pipelines_table.vue';
+import EmptyState from '../components/empty_state.vue';
+import SvgBlankState from '../components/blank_state.vue';
+import LoadingIcon from '../../vue_shared/components/loading_icon.vue';
+import PipelinesTableComponent from '../components/pipelines_table.vue';
import eventHub from '../event_hub';
export default {
components: {
- pipelinesTableComponent,
- errorState,
- emptyState,
- loadingIcon,
- },
- computed: {
- shouldRenderErrorState() {
- return this.hasError && !this.isLoading;
- },
+ PipelinesTableComponent,
+ SvgBlankState,
+ EmptyState,
+ LoadingIcon,
},
data() {
return {
@@ -85,6 +81,7 @@ export default {
this.hasError = true;
this.isLoading = false;
this.updateGraphDropdown = false;
+ this.hasMadeRequest = true;
},
setIsMakingRequest(isMakingRequest) {
this.isMakingRequest = isMakingRequest;
@@ -96,7 +93,7 @@ export default {
postAction(endpoint) {
this.service.postAction(endpoint)
.then(() => eventHub.$emit('refreshPipelines'))
- .catch(() => new Flash('An error occurred while making the request.'));
+ .catch(() => Flash(__('An error occurred while making the request.')));
},
},
};
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 705a60b3ba2..6b26708148c 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -9,7 +9,7 @@ import eventHub from './event_hub';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
const mediator = new PipelinesMediator({ endpoint: dataset.endpoint });
@@ -70,4 +70,4 @@ document.addEventListener('DOMContentLoaded', () => {
});
},
});
-});
+};
diff --git a/app/assets/javascripts/pipelines/pipelines_bundle.js b/app/assets/javascripts/pipelines/pipelines_bundle.js
deleted file mode 100644
index ab5596e70f0..00000000000
--- a/app/assets/javascripts/pipelines/pipelines_bundle.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import PipelinesStore from './stores/pipelines_store';
-import pipelinesComponent from './components/pipelines.vue';
-import Translate from '../vue_shared/translate';
-
-Vue.use(Translate);
-
-document.addEventListener('DOMContentLoaded', () => new Vue({
- el: '#pipelines-list-vue',
- components: {
- pipelinesComponent,
- },
- data() {
- const store = new PipelinesStore();
-
- return {
- store,
- };
- },
- render(createElement) {
- return createElement('pipelines-component', {
- props: {
- store: this.store,
- },
- });
- },
-}));
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index e2285494e62..47736fc5f42 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this */
import Vue from 'vue';
import VueResource from 'vue-resource';
+import '../../vue_shared/vue_resource_interceptor';
Vue.use(VueResource);
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index 930f0fb381e..a811781853b 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -1,103 +1,85 @@
/* eslint-disable comma-dangle, no-unused-vars, class-methods-use-this, quotes, consistent-return, func-names, prefer-arrow-callback, space-before-function-paren, max-len */
import Cookies from 'js-cookie';
-import { getPagePath } from '~/lib/utils/common_utils';
import axios from '~/lib/utils/axios_utils';
import { __ } from '~/locale';
import flash from '../flash';
-((global) => {
- class Profile {
- constructor({ form } = {}) {
- this.onSubmitForm = this.onSubmitForm.bind(this);
- this.form = form || $('.edit-user');
- this.newRepoActivated = Cookies.get('new_repo');
- this.setRepoRadio();
- this.bindEvents();
- this.initAvatarGlCrop();
- }
-
- initAvatarGlCrop() {
- const cropOpts = {
- filename: '.js-avatar-filename',
- previewImage: '.avatar-image .avatar',
- modalCrop: '.modal-profile-crop',
- pickImageEl: '.js-choose-user-avatar-button',
- uploadImageBtn: '.js-upload-user-avatar',
- modalCropImg: '.modal-profile-crop-image'
- };
- this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
- }
+export default class Profile {
+ constructor({ form } = {}) {
+ this.onSubmitForm = this.onSubmitForm.bind(this);
+ this.form = form || $('.edit-user');
+ this.newRepoActivated = Cookies.get('new_repo');
+ this.setRepoRadio();
+ this.bindEvents();
+ this.initAvatarGlCrop();
+ }
- bindEvents() {
- $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
- $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
- $('#user_notification_email').on('change', this.submitForm);
- $('#user_notified_of_own_activity').on('change', this.submitForm);
- this.form.on('submit', this.onSubmitForm);
- }
+ initAvatarGlCrop() {
+ const cropOpts = {
+ filename: '.js-avatar-filename',
+ previewImage: '.avatar-image .avatar',
+ modalCrop: '.modal-profile-crop',
+ pickImageEl: '.js-choose-user-avatar-button',
+ uploadImageBtn: '.js-upload-user-avatar',
+ modalCropImg: '.modal-profile-crop-image'
+ };
+ this.avatarGlCrop = $('.js-user-avatar-input').glCrop(cropOpts).data('glcrop');
+ }
- submitForm() {
- return $(this).parents('form').submit();
- }
+ bindEvents() {
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('input[name="user[multi_file]"]').on('change', this.setNewRepoCookie);
+ $('#user_notification_email').on('change', this.submitForm);
+ $('#user_notified_of_own_activity').on('change', this.submitForm);
+ this.form.on('submit', this.onSubmitForm);
+ }
- onSubmitForm(e) {
- e.preventDefault();
- return this.saveForm();
- }
+ submitForm() {
+ return $(this).parents('form').submit();
+ }
- saveForm() {
- const self = this;
- const formData = new FormData(this.form[0]);
- const avatarBlob = this.avatarGlCrop.getBlob();
+ onSubmitForm(e) {
+ e.preventDefault();
+ return this.saveForm();
+ }
- if (avatarBlob != null) {
- formData.append('user[avatar]', avatarBlob, 'avatar.png');
- }
+ saveForm() {
+ const self = this;
+ const formData = new FormData(this.form[0]);
+ const avatarBlob = this.avatarGlCrop.getBlob();
- axios({
- method: this.form.attr('method'),
- url: this.form.attr('action'),
- data: formData,
- })
- .then(({ data }) => flash(data.message, 'notice'))
- .then(() => {
- window.scrollTo(0, 0);
- // Enable submit button after requests ends
- self.form.find(':input[disabled]').enable();
- })
- .catch(error => flash(error.message));
+ if (avatarBlob != null) {
+ formData.append('user[avatar]', avatarBlob, 'avatar.png');
}
- setNewRepoCookie() {
- if (this.value === 'off') {
- Cookies.remove('new_repo');
- } else {
- Cookies.set('new_repo', true, { expires_in: 365 });
- }
- }
+ axios({
+ method: this.form.attr('method'),
+ url: this.form.attr('action'),
+ data: formData,
+ })
+ .then(({ data }) => flash(data.message, 'notice'))
+ .then(() => {
+ window.scrollTo(0, 0);
+ // Enable submit button after requests ends
+ self.form.find(':input[disabled]').enable();
+ })
+ .catch(error => flash(error.message));
+ }
- setRepoRadio() {
- const multiEditRadios = $('input[name="user[multi_file]"]');
- if (this.newRepoActivated || this.newRepoActivated === 'true') {
- multiEditRadios.filter('[value=on]').prop('checked', true);
- } else {
- multiEditRadios.filter('[value=off]').prop('checked', true);
- }
+ setNewRepoCookie() {
+ if (this.value === 'off') {
+ Cookies.remove('new_repo');
+ } else {
+ Cookies.set('new_repo', true, { expires_in: 365 });
}
}
- $(function() {
- $(document).on('input.ssh_key', '#key_key', function() {
- const $title = $('#key_title');
- const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
-
- // Extract the SSH Key title from its comment
- if (comment && comment.length > 1) {
- return $title.val(comment[1]).change();
- }
- });
- if (getPagePath() === 'profiles') {
- return new Profile();
+ setRepoRadio() {
+ const multiEditRadios = $('input[name="user[multi_file]"]');
+ if (this.newRepoActivated || this.newRepoActivated === 'true') {
+ multiEditRadios.filter('[value=on]').prop('checked', true);
+ } else {
+ multiEditRadios.filter('[value=off]').prop('checked', true);
}
- });
-})(window.gl || (window.gl = {}));
+ }
+}
diff --git a/app/assets/javascripts/profile/profile_bundle.js b/app/assets/javascripts/profile/profile_bundle.js
deleted file mode 100644
index ff35a9bcb83..00000000000
--- a/app/assets/javascripts/profile/profile_bundle.js
+++ /dev/null
@@ -1,2 +0,0 @@
-import './gl_crop';
-import './profile';
diff --git a/app/assets/javascripts/protected_branches/index.js b/app/assets/javascripts/protected_branches/index.js
deleted file mode 100644
index c9e7af127d2..00000000000
--- a/app/assets/javascripts/protected_branches/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-import ProtectedBranchCreate from './protected_branch_create';
-import ProtectedBranchEditList from './protected_branch_edit_list';
-
-$(() => {
- const protectedBranchCreate = new ProtectedBranchCreate();
- const protectedBranchEditList = new ProtectedBranchEditList();
-});
diff --git a/app/assets/javascripts/protected_tags/index.js b/app/assets/javascripts/protected_tags/index.js
deleted file mode 100644
index b1618e24e49..00000000000
--- a/app/assets/javascripts/protected_tags/index.js
+++ /dev/null
@@ -1,9 +0,0 @@
-/* eslint-disable no-unused-vars */
-
-import ProtectedTagCreate from './protected_tag_create';
-import ProtectedTagEditList from './protected_tag_edit_list';
-
-$(() => {
- const protectedtTagCreate = new ProtectedTagCreate();
- const protectedtTagEditList = new ProtectedTagEditList();
-});
diff --git a/app/assets/javascripts/registry/index.js b/app/assets/javascripts/registry/index.js
index d8edff73f72..6fb125192b2 100644
--- a/app/assets/javascripts/registry/index.js
+++ b/app/assets/javascripts/registry/index.js
@@ -4,7 +4,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => new Vue({
+export default () => new Vue({
el: '#js-vue-registry-images',
components: {
registryApp,
@@ -22,4 +22,4 @@ document.addEventListener('DOMContentLoaded', () => new Vue({
},
});
},
-}));
+});
diff --git a/app/assets/javascripts/shortcuts_issuable.js b/app/assets/javascripts/shortcuts_issuable.js
index 689befc742e..14545824e74 100644
--- a/app/assets/javascripts/shortcuts_issuable.js
+++ b/app/assets/javascripts/shortcuts_issuable.js
@@ -9,13 +9,12 @@ export default class ShortcutsIssuable extends Shortcuts {
super();
this.$replyField = isMergeRequest ? $('.js-main-target-form #note_note') : $('.js-main-target-form .js-vue-comment-form');
- this.editBtn = document.querySelector('.js-issuable-edit');
Mousetrap.bind('a', () => ShortcutsIssuable.openSidebarDropdown('assignee'));
Mousetrap.bind('m', () => ShortcutsIssuable.openSidebarDropdown('milestone'));
Mousetrap.bind('l', () => ShortcutsIssuable.openSidebarDropdown('labels'));
Mousetrap.bind('r', this.replyWithSelectedText.bind(this));
- Mousetrap.bind('e', this.editIssue.bind(this));
+ Mousetrap.bind('e', ShortcutsIssuable.editIssue);
if (isMergeRequest) {
this.enabledHelp.push('.hidden-shortcut.merge_requests');
@@ -58,10 +57,10 @@ export default class ShortcutsIssuable extends Shortcuts {
return false;
}
- editIssue() {
+ static editIssue() {
// Need to click the element as on issues, editing is inline
// on merge request, editing is on a different page
- this.editBtn.click();
+ document.querySelector('.js-issuable-edit').click();
return false;
}
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.js b/app/assets/javascripts/sidebar/components/assignees/assignees.js
deleted file mode 100644
index 643877b9d47..00000000000
--- a/app/assets/javascripts/sidebar/components/assignees/assignees.js
+++ /dev/null
@@ -1,224 +0,0 @@
-export default {
- name: 'Assignees',
- data() {
- return {
- defaultRenderCount: 5,
- defaultMaxCounter: 99,
- showLess: true,
- };
- },
- props: {
- rootPath: {
- type: String,
- required: true,
- },
- users: {
- type: Array,
- required: true,
- },
- editable: {
- type: Boolean,
- required: true,
- },
- },
- computed: {
- firstUser() {
- return this.users[0];
- },
- hasMoreThanTwoAssignees() {
- return this.users.length > 2;
- },
- hasMoreThanOneAssignee() {
- return this.users.length > 1;
- },
- hasAssignees() {
- return this.users.length > 0;
- },
- hasNoUsers() {
- return !this.users.length;
- },
- hasOneUser() {
- return this.users.length === 1;
- },
- renderShowMoreSection() {
- return this.users.length > this.defaultRenderCount;
- },
- numberOfHiddenAssignees() {
- return this.users.length - this.defaultRenderCount;
- },
- isHiddenAssignees() {
- return this.numberOfHiddenAssignees > 0;
- },
- hiddenAssigneesLabel() {
- return `+ ${this.numberOfHiddenAssignees} more`;
- },
- collapsedTooltipTitle() {
- const maxRender = Math.min(this.defaultRenderCount, this.users.length);
- const renderUsers = this.users.slice(0, maxRender);
- const names = renderUsers.map(u => u.name);
-
- if (this.users.length > maxRender) {
- names.push(`+ ${this.users.length - maxRender} more`);
- }
-
- return names.join(', ');
- },
- sidebarAvatarCounter() {
- let counter = `+${this.users.length - 1}`;
-
- if (this.users.length > this.defaultMaxCounter) {
- counter = `${this.defaultMaxCounter}+`;
- }
-
- return counter;
- },
- },
- methods: {
- assignSelf() {
- this.$emit('assign-self');
- },
- toggleShowLess() {
- this.showLess = !this.showLess;
- },
- renderAssignee(index) {
- return !this.showLess || (index < this.defaultRenderCount && this.showLess);
- },
- avatarUrl(user) {
- return user.avatar || user.avatar_url || gon.default_avatar_url;
- },
- assigneeUrl(user) {
- return `${this.rootPath}${user.username}`;
- },
- assigneeAlt(user) {
- return `${user.name}'s avatar`;
- },
- assigneeUsername(user) {
- return `@${user.username}`;
- },
- shouldRenderCollapsedAssignee(index) {
- const firstTwo = this.users.length <= 2 && index <= 2;
-
- return index === 0 || firstTwo;
- },
- },
- template: `
- <div>
- <div
- class="sidebar-collapsed-icon sidebar-collapsed-user"
- :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
- data-container="body"
- data-placement="left"
- :title="collapsedTooltipTitle"
- >
- <i
- v-if="hasNoUsers"
- aria-label="No Assignee"
- class="fa fa-user"
- />
- <button
- type="button"
- class="btn-link"
- v-for="(user, index) in users"
- v-if="shouldRenderCollapsedAssignee(index)"
- >
- <img
- width="24"
- class="avatar avatar-inline s24"
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- />
- <span class="author">
- {{ user.name }}
- </span>
- </button>
- <button
- v-if="hasMoreThanTwoAssignees"
- class="btn-link"
- type="button"
- >
- <span
- class="avatar-counter sidebar-avatar-counter"
- >
- {{ sidebarAvatarCounter }}
- </span>
- </button>
- </div>
- <div class="value hide-collapsed">
- <template v-if="hasNoUsers">
- <span class="assign-yourself no-value">
- No assignee
- <template v-if="editable">
- -
- <button
- type="button"
- class="btn-link"
- @click="assignSelf"
- >
- assign yourself
- </button>
- </template>
- </span>
- </template>
- <template v-else-if="hasOneUser">
- <a
- class="author_link bold"
- :href="assigneeUrl(firstUser)"
- >
- <img
- width="32"
- class="avatar avatar-inline s32"
- :alt="assigneeAlt(firstUser)"
- :src="avatarUrl(firstUser)"
- />
- <span class="author">
- {{ firstUser.name }}
- </span>
- <span class="username">
- {{ assigneeUsername(firstUser) }}
- </span>
- </a>
- </template>
- <template v-else>
- <div class="user-list">
- <div
- class="user-item"
- v-for="(user, index) in users"
- v-if="renderAssignee(index)"
- >
- <a
- class="user-link has-tooltip"
- data-placement="bottom"
- :href="assigneeUrl(user)"
- :data-title="user.name"
- >
- <img
- width="32"
- class="avatar avatar-inline s32"
- :alt="assigneeAlt(user)"
- :src="avatarUrl(user)"
- />
- </a>
- </div>
- </div>
- <div
- v-if="renderShowMoreSection"
- class="user-list-more"
- >
- <button
- type="button"
- class="btn-link"
- @click="toggleShowLess"
- >
- <template v-if="showLess">
- {{ hiddenAssigneesLabel }}
- </template>
- <template v-else>
- - show less
- </template>
- </button>
- </div>
- </template>
- </div>
- </div>
- `,
-};
diff --git a/app/assets/javascripts/sidebar/components/assignees/assignees.vue b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
new file mode 100644
index 00000000000..1e7f46454bf
--- /dev/null
+++ b/app/assets/javascripts/sidebar/components/assignees/assignees.vue
@@ -0,0 +1,232 @@
+<script>
+export default {
+ name: 'Assignees',
+ props: {
+ rootPath: {
+ type: String,
+ required: true,
+ },
+ users: {
+ type: Array,
+ required: true,
+ },
+ editable: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ defaultRenderCount: 5,
+ defaultMaxCounter: 99,
+ showLess: true,
+ };
+ },
+ computed: {
+ firstUser() {
+ return this.users[0];
+ },
+ hasMoreThanTwoAssignees() {
+ return this.users.length > 2;
+ },
+ hasMoreThanOneAssignee() {
+ return this.users.length > 1;
+ },
+ hasAssignees() {
+ return this.users.length > 0;
+ },
+ hasNoUsers() {
+ return !this.users.length;
+ },
+ hasOneUser() {
+ return this.users.length === 1;
+ },
+ renderShowMoreSection() {
+ return this.users.length > this.defaultRenderCount;
+ },
+ numberOfHiddenAssignees() {
+ return this.users.length - this.defaultRenderCount;
+ },
+ isHiddenAssignees() {
+ return this.numberOfHiddenAssignees > 0;
+ },
+ hiddenAssigneesLabel() {
+ return `+ ${this.numberOfHiddenAssignees} more`;
+ },
+ collapsedTooltipTitle() {
+ const maxRender = Math.min(this.defaultRenderCount, this.users.length);
+ const renderUsers = this.users.slice(0, maxRender);
+ const names = renderUsers.map(u => u.name);
+
+ if (this.users.length > maxRender) {
+ names.push(`+ ${this.users.length - maxRender} more`);
+ }
+
+ return names.join(', ');
+ },
+ sidebarAvatarCounter() {
+ let counter = `+${this.users.length - 1}`;
+
+ if (this.users.length > this.defaultMaxCounter) {
+ counter = `${this.defaultMaxCounter}+`;
+ }
+
+ return counter;
+ },
+ },
+ methods: {
+ assignSelf() {
+ this.$emit('assign-self');
+ },
+ toggleShowLess() {
+ this.showLess = !this.showLess;
+ },
+ renderAssignee(index) {
+ return !this.showLess || (index < this.defaultRenderCount && this.showLess);
+ },
+ avatarUrl(user) {
+ return user.avatar || user.avatar_url || gon.default_avatar_url;
+ },
+ assigneeUrl(user) {
+ return `${this.rootPath}${user.username}`;
+ },
+ assigneeAlt(user) {
+ return `${user.name}'s avatar`;
+ },
+ assigneeUsername(user) {
+ return `@${user.username}`;
+ },
+ shouldRenderCollapsedAssignee(index) {
+ const firstTwo = this.users.length <= 2 && index <= 2;
+
+ return index === 0 || firstTwo;
+ },
+ },
+};
+</script>
+
+<template>
+ <div>
+ <div
+ class="sidebar-collapsed-icon sidebar-collapsed-user"
+ :class="{ 'multiple-users': hasMoreThanOneAssignee, 'has-tooltip': hasAssignees }"
+ data-container="body"
+ data-placement="left"
+ :title="collapsedTooltipTitle"
+ >
+ <i
+ v-if="hasNoUsers"
+ aria-label="No Assignee"
+ class="fa fa-user"
+ >
+ </i>
+ <button
+ type="button"
+ class="btn-link"
+ v-for="(user, index) in users"
+ v-if="shouldRenderCollapsedAssignee(index)"
+ :key="user.id"
+ >
+ <img
+ width="24"
+ class="avatar avatar-inline s24"
+ :alt="assigneeAlt(user)"
+ :src="avatarUrl(user)"
+ />
+ <span class="author">
+ {{ user.name }}
+ </span>
+ </button>
+ <button
+ v-if="hasMoreThanTwoAssignees"
+ class="btn-link"
+ type="button"
+ >
+ <span
+ class="avatar-counter sidebar-avatar-counter"
+ >
+ {{ sidebarAvatarCounter }}
+ </span>
+ </button>
+ </div>
+ <div class="value hide-collapsed">
+ <template v-if="hasNoUsers">
+ <span class="assign-yourself no-value">
+ No assignee
+ <template v-if="editable">
+ -
+ <button
+ type="button"
+ class="btn-link"
+ @click="assignSelf"
+ >
+ assign yourself
+ </button>
+ </template>
+ </span>
+ </template>
+ <template v-else-if="hasOneUser">
+ <a
+ class="author_link bold"
+ :href="assigneeUrl(firstUser)"
+ >
+ <img
+ width="32"
+ class="avatar avatar-inline s32"
+ :alt="assigneeAlt(firstUser)"
+ :src="avatarUrl(firstUser)"
+ />
+ <span class="author">
+ {{ firstUser.name }}
+ </span>
+ <span class="username">
+ {{ assigneeUsername(firstUser) }}
+ </span>
+ </a>
+ </template>
+ <template v-else>
+ <div class="user-list">
+ <div
+ class="user-item"
+ v-for="(user, index) in users"
+ v-if="renderAssignee(index)"
+ :key="user.id"
+ >
+ <a
+ class="user-link has-tooltip"
+ data-container="body"
+ data-placement="bottom"
+ :href="assigneeUrl(user)"
+ :data-title="user.name"
+ >
+ <img
+ width="32"
+ class="avatar avatar-inline s32"
+ :alt="assigneeAlt(user)"
+ :src="avatarUrl(user)"
+ />
+ </a>
+ </div>
+ </div>
+ <div
+ v-if="renderShowMoreSection"
+ class="user-list-more"
+ >
+ <button
+ type="button"
+ class="btn-link"
+ @click="toggleShowLess"
+ >
+ <template v-if="showLess">
+ {{ hiddenAssigneesLabel }}
+ </template>
+ <template v-else>
+ - show less
+ </template>
+ </button>
+ </div>
+ </template>
+ </div>
+ </div>
+</template>
+
diff --git a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
index 9e47039d920..8269fe1281d 100644
--- a/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
+++ b/app/assets/javascripts/sidebar/components/assignees/sidebar_assignees.js
@@ -1,6 +1,6 @@
import Flash from '../../../flash';
import AssigneeTitle from './assignee_title';
-import Assignees from './assignees';
+import Assignees from './assignees.vue';
import Store from '../../stores/sidebar_store';
import eventHub from '../../event_hub';
@@ -28,8 +28,8 @@ export default {
},
},
components: {
- 'assignee-title': AssigneeTitle,
- assignees: Assignees,
+ AssigneeTitle,
+ Assignees,
},
methods: {
assignSelf() {
diff --git a/app/assets/javascripts/sidebar/sidebar_bundle.js b/app/assets/javascripts/sidebar/sidebar_bundle.js
index 04c39d7b6b5..377846db70e 100644
--- a/app/assets/javascripts/sidebar/sidebar_bundle.js
+++ b/app/assets/javascripts/sidebar/sidebar_bundle.js
@@ -1,13 +1,9 @@
import Mediator from './sidebar_mediator';
import { mountSidebar, getSidebarOptions } from './mount_sidebar';
-function domContentLoaded() {
+export default () => {
const mediator = new Mediator(getSidebarOptions());
mediator.fetch();
mountSidebar(mediator);
-}
-
-document.addEventListener('DOMContentLoaded', domContentLoaded);
-
-export default domContentLoaded;
+};
diff --git a/app/assets/javascripts/snippet/snippet_bundle.js b/app/assets/javascripts/snippet/snippet_bundle.js
index a98403f4cf2..ce0fd3f6ff8 100644
--- a/app/assets/javascripts/snippet/snippet_bundle.js
+++ b/app/assets/javascripts/snippet/snippet_bundle.js
@@ -1,12 +1,9 @@
-/* eslint-disable func-names, space-before-function-paren, prefer-arrow-callback, no-var, quotes, max-len */
/* global ace */
-(function() {
- $(function() {
- var editor = ace.edit("editor");
+export default () => {
+ const editor = ace.edit('editor');
- $(".snippet-form-holder form").on('submit', function() {
- $(".snippet-file-content").val(editor.getValue());
- });
+ $('.snippet-form-holder form').on('submit', () => {
+ $('.snippet-file-content').val(editor.getValue());
});
-}).call(window);
+};
diff --git a/app/assets/javascripts/terminal/terminal_bundle.js b/app/assets/javascripts/terminal/index.js
index 134522ef961..1a75e072c4e 100644
--- a/app/assets/javascripts/terminal/terminal_bundle.js
+++ b/app/assets/javascripts/terminal/index.js
@@ -6,4 +6,4 @@ import './terminal';
window.Terminal = Terminal;
-$(() => new gl.Terminal({ selector: '#terminal' }));
+export default () => new gl.Terminal({ selector: '#terminal' });
diff --git a/app/assets/javascripts/test.js b/app/assets/javascripts/test.js
deleted file mode 100644
index c4c7918a68f..00000000000
--- a/app/assets/javascripts/test.js
+++ /dev/null
@@ -1 +0,0 @@
-$.fx.off = true;
diff --git a/app/assets/javascripts/u2f/authenticate.js b/app/assets/javascripts/u2f/authenticate.js
index a3cc04e35fe..fd42f9c3baa 100644
--- a/app/assets/javascripts/u2f/authenticate.js
+++ b/app/assets/javascripts/u2f/authenticate.js
@@ -1,7 +1,5 @@
-/* eslint-disable func-names, wrap-iife */
-/* global u2f */
import _ from 'underscore';
-import isU2FSupported from './util';
+import importU2FLibrary from './util';
import U2FError from './error';
// Authenticate U2F (universal 2nd factor) devices for users to authenticate with.
@@ -10,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup
export default class U2FAuthenticate {
constructor(container, form, u2fParams, fallbackButton, fallbackUI) {
+ this.u2fUtils = null;
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderAuthenticated = this.renderAuthenticated.bind(this);
@@ -50,22 +49,23 @@ export default class U2FAuthenticate {
}
start() {
- if (isU2FSupported()) {
- return this.renderInProgress();
- }
- return this.renderNotSupported();
+ return importU2FLibrary()
+ .then((utils) => {
+ this.u2fUtils = utils;
+ this.renderInProgress();
+ })
+ .catch(() => this.renderNotSupported());
}
authenticate() {
- return u2f.sign(this.appId, this.challenge, this.signRequests, (function (_this) {
- return function (response) {
+ return this.u2fUtils.sign(this.appId, this.challenge, this.signRequests,
+ (response) => {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'authenticate');
- return _this.renderError(error);
+ return this.renderError(error);
}
- return _this.renderAuthenticated(JSON.stringify(response));
- };
- })(this), 10);
+ return this.renderAuthenticated(JSON.stringify(response));
+ }, 10);
}
renderTemplate(name, params) {
diff --git a/app/assets/javascripts/u2f/register.js b/app/assets/javascripts/u2f/register.js
index cc3f02e75f6..869fac658e8 100644
--- a/app/assets/javascripts/u2f/register.js
+++ b/app/assets/javascripts/u2f/register.js
@@ -1,8 +1,5 @@
-/* eslint-disable func-names, wrap-iife */
-/* global u2f */
-
import _ from 'underscore';
-import isU2FSupported from './util';
+import importU2FLibrary from './util';
import U2FError from './error';
// Register U2F (universal 2nd factor) devices for users to authenticate with.
@@ -11,6 +8,7 @@ import U2FError from './error';
// State Flow #2: setup -> in_progress -> error -> setup
export default class U2FRegister {
constructor(container, u2fParams) {
+ this.u2fUtils = null;
this.container = container;
this.renderNotSupported = this.renderNotSupported.bind(this);
this.renderRegistered = this.renderRegistered.bind(this);
@@ -34,22 +32,23 @@ export default class U2FRegister {
}
start() {
- if (isU2FSupported()) {
- return this.renderSetup();
- }
- return this.renderNotSupported();
+ return importU2FLibrary()
+ .then((utils) => {
+ this.u2fUtils = utils;
+ this.renderSetup();
+ })
+ .catch(() => this.renderNotSupported());
}
register() {
- return u2f.register(this.appId, this.registerRequests, this.signRequests, (function (_this) {
- return function (response) {
+ return this.u2fUtils.register(this.appId, this.registerRequests, this.signRequests,
+ (response) => {
if (response.errorCode) {
const error = new U2FError(response.errorCode, 'register');
- return _this.renderError(error);
+ return this.renderError(error);
}
- return _this.renderRegistered(JSON.stringify(response));
- };
- })(this), 10);
+ return this.renderRegistered(JSON.stringify(response));
+ }, 10);
}
renderTemplate(name, params) {
diff --git a/app/assets/javascripts/u2f/util.js b/app/assets/javascripts/u2f/util.js
index 9771ff935c2..5778f00332d 100644
--- a/app/assets/javascripts/u2f/util.js
+++ b/app/assets/javascripts/u2f/util.js
@@ -1,3 +1,41 @@
-export default function isU2FSupported() {
- return window.u2f;
+function isOpera(userAgent) {
+ return userAgent.indexOf('Opera') >= 0 || userAgent.indexOf('OPR') >= 0;
+}
+
+function getOperaVersion(userAgent) {
+ const match = userAgent.match(/OPR[^0-9]*([0-9]+)[^0-9]+/);
+ return match ? parseInt(match[1], 10) : false;
+}
+
+function isChrome(userAgent) {
+ return userAgent.indexOf('Chrom') >= 0 && !isOpera(userAgent);
+}
+
+function getChromeVersion(userAgent) {
+ const match = userAgent.match(/Chrom(?:e|ium)\/([0-9]+)\./);
+ return match ? parseInt(match[1], 10) : false;
+}
+
+export function canInjectU2fApi(userAgent) {
+ const isSupportedChrome = isChrome(userAgent) && getChromeVersion(userAgent) >= 41;
+ const isSupportedOpera = isOpera(userAgent) && getOperaVersion(userAgent) >= 40;
+ const isMobile = (
+ userAgent.indexOf('droid') >= 0 ||
+ userAgent.indexOf('CriOS') >= 0 ||
+ /\b(iPad|iPhone|iPod)(?=;)/.test(userAgent)
+ );
+ return (isSupportedChrome || isSupportedOpera) && !isMobile;
+}
+
+export default function importU2FLibrary() {
+ if (window.u2f) {
+ return Promise.resolve(window.u2f);
+ }
+
+ const userAgent = typeof navigator !== 'undefined' ? navigator.userAgent : '';
+ if (canInjectU2fApi(userAgent) || (gon && gon.test_env)) {
+ return import(/* webpackMode: "eager" */ 'vendor/u2f').then(() => window.u2f);
+ }
+
+ return Promise.reject();
}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index 109a302a172..54a98abf860 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -1,8 +1,8 @@
<script>
/* eslint-disable vue/require-default-prop */
- import pipelineStage from '../../pipelines/components/stage.vue';
- import ciIcon from '../../vue_shared/components/ci_icon.vue';
- import icon from '../../vue_shared/components/icon.vue';
+ import pipelineStage from '~/pipelines/components/stage.vue';
+ import ciIcon from '~/vue_shared/components/ci_icon.vue';
+ import icon from '~/vue_shared/components/icon.vue';
export default {
name: 'MRWidgetPipeline',
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
index 7ba6c29006a..162f048aac7 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js
@@ -227,7 +227,8 @@ export default {
@click="handleMergeButtonClick()"
:disabled="isMergeButtonDisabled"
:class="mergeButtonClass"
- type="button">
+ type="button"
+ class="qa-merge-button">
<i
v-if="isMakingRequest"
class="fa fa-spinner fa-spin"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
index e9f23b0b113..143fd328d88 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue
@@ -111,7 +111,7 @@ js-toggle-container accept-action media space-children"
>
<button
type="button"
- class="btn btn-sm btn-reopen btn-success"
+ class="btn btn-sm btn-reopen btn-success qa-mr-rebase-button"
:disabled="isMakingRequest"
@click="rebase"
>
diff --git a/app/assets/javascripts/vue_merge_request_widget/index.js b/app/assets/javascripts/vue_merge_request_widget/index.js
index 6b9918b65b0..69a9132a2da 100644
--- a/app/assets/javascripts/vue_merge_request_widget/index.js
+++ b/app/assets/javascripts/vue_merge_request_widget/index.js
@@ -6,7 +6,7 @@ import Translate from '../vue_shared/translate';
Vue.use(Translate);
-document.addEventListener('DOMContentLoaded', () => {
+export default () => {
gl.mrWidgetData.gitlabLogo = gon.gitlab_logo;
const vm = new Vue(mrWidgetOptions);
@@ -14,4 +14,4 @@ document.addEventListener('DOMContentLoaded', () => {
window.gl.mrWidget = {
checkStatus: vm.checkStatus,
};
-});
+};
diff --git a/app/assets/javascripts/vue_shared/components/clipboard_button.vue b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
index 31d9b9d9c48..3b6c2da1664 100644
--- a/app/assets/javascripts/vue_shared/components/clipboard_button.vue
+++ b/app/assets/javascripts/vue_shared/components/clipboard_button.vue
@@ -1,8 +1,8 @@
<script>
- import tooltip from '../directives/tooltip';
/**
* Falls back to the code used in `copy_to_clipboard.js`
*/
+ import tooltip from '../directives/tooltip';
export default {
name: 'ClipboardButton',
@@ -28,6 +28,11 @@
required: false,
default: false,
},
+ cssClass: {
+ type: String,
+ required: false,
+ default: 'btn btn-default btn-transparent btn-clipboard',
+ },
},
};
</script>
@@ -35,7 +40,7 @@
<template>
<button
type="button"
- class="btn btn-transparent btn-clipboard"
+ :class="cssClass"
:title="title"
:data-clipboard-text="text"
v-tooltip
diff --git a/app/assets/javascripts/vue_shared/components/expand_button.vue b/app/assets/javascripts/vue_shared/components/expand_button.vue
index 3595a9389e9..c943c8d98a4 100644
--- a/app/assets/javascripts/vue_shared/components/expand_button.vue
+++ b/app/assets/javascripts/vue_shared/components/expand_button.vue
@@ -39,7 +39,7 @@
@click="onClick">
...
</button>
- <span v-show="!isCollapsed">
+ <span v-if="!isCollapsed">
<slot name="expanded"></slot>
</span>
</span>
diff --git a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
index b48828ae81f..3d39b3ab173 100644
--- a/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
+++ b/app/assets/javascripts/vue_shared/components/issue/issue_warning.vue
@@ -11,14 +11,12 @@
default: false,
required: false,
},
-
isConfidential: {
type: Boolean,
default: false,
required: false,
},
},
-
computed: {
warningIcon() {
if (this.isConfidential) return 'eye-slash';
@@ -26,7 +24,6 @@
return '';
},
-
isLockedAndConfidential() {
return this.isConfidential && this.isLocked;
},
diff --git a/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
new file mode 100644
index 00000000000..80e3db52cb0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/notes/skeleton_note.vue
@@ -0,0 +1,24 @@
+<template>
+ <li class="timeline-entry note">
+ <div class="timeline-entry-inner">
+ <div class="timeline-icon">
+ </div>
+ <div class="timeline-content">
+ <div class="note-header"></div>
+ <div class="note-body">
+ <skeleton-loading-container />
+ </div>
+ </div>
+ </div>
+ </li>
+</template>
+
+<script>
+ import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
+
+ export default {
+ components: {
+ skeletonLoadingContainer,
+ },
+ };
+</script>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
index 1413dd69f24..3fcacd156c5 100644
--- a/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
+++ b/app/assets/javascripts/vue_shared/components/sidebar/date_picker.vue
@@ -14,6 +14,11 @@
collapsedCalendarIcon,
},
props: {
+ blockClass: {
+ type: String,
+ required: false,
+ default: '',
+ },
collapsed: {
type: Boolean,
required: false,
@@ -91,7 +96,10 @@
</script>
<template>
- <div class="block">
+ <div
+ class="block"
+ :class="blockClass"
+ >
<div class="issuable-sidebar-header">
<toggle-sidebar
:collapsed="collapsed"
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
new file mode 100644
index 00000000000..3b17135f0e5
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/base.vue
@@ -0,0 +1,149 @@
+<script>
+import LabelsSelect from '~/labels_select';
+import LoadingIcon from '../../loading_icon.vue';
+
+import DropdownTitle from './dropdown_title.vue';
+import DropdownValue from './dropdown_value.vue';
+import DropdownValueCollapsed from './dropdown_value_collapsed.vue';
+import DropdownButton from './dropdown_button.vue';
+import DropdownHiddenInput from './dropdown_hidden_input.vue';
+import DropdownHeader from './dropdown_header.vue';
+import DropdownSearchInput from './dropdown_search_input.vue';
+import DropdownFooter from './dropdown_footer.vue';
+import DropdownCreateLabel from './dropdown_create_label.vue';
+
+export default {
+ components: {
+ LoadingIcon,
+ DropdownTitle,
+ DropdownValue,
+ DropdownValueCollapsed,
+ DropdownButton,
+ DropdownHiddenInput,
+ DropdownHeader,
+ DropdownSearchInput,
+ DropdownFooter,
+ DropdownCreateLabel,
+ },
+ props: {
+ showCreate: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ abilityName: {
+ type: String,
+ required: true,
+ },
+ context: {
+ type: Object,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ updatePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ labelsWebUrl: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ labelFilterBasePath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ canEdit: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+ computed: {
+ hiddenInputName() {
+ return this.showCreate ? `${this.abilityName}[label_names][]` : 'label_id[]';
+ },
+ },
+ mounted() {
+ this.labelsDropdown = new LabelsSelect(this.$refs.dropdownButton, {
+ handleClick: this.handleClick,
+ });
+ },
+ methods: {
+ handleClick(label) {
+ this.$emit('onLabelClick', label);
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="block labels">
+ <dropdown-value-collapsed
+ v-if="showCreate"
+ :labels="context.labels"
+ />
+ <dropdown-title
+ :can-edit="canEdit"
+ />
+ <dropdown-value
+ :labels="context.labels"
+ :label-filter-base-path="labelFilterBasePath"
+ >
+ <slot></slot>
+ </dropdown-value>
+ <div
+ v-if="canEdit"
+ class="selectbox"
+ style="display: none;"
+ >
+ <dropdown-hidden-input
+ v-for="label in context.labels"
+ :key="label.id"
+ :name="hiddenInputName"
+ :label="label"
+ />
+ <div class="dropdown">
+ <dropdown-button
+ :ability-name="abilityName"
+ :field-name="hiddenInputName"
+ :update-path="updatePath"
+ :labels-path="labelsPath"
+ :namespace="namespace"
+ :labels="context.labels"
+ :show-extra-options="!showCreate"
+ />
+ <div
+ class="dropdown-menu dropdown-select dropdown-menu-paging
+dropdown-menu-labels dropdown-menu-selectable"
+ >
+ <div class="dropdown-page-one">
+ <dropdown-header v-if="showCreate" />
+ <dropdown-search-input/>
+ <div class="dropdown-content"></div>
+ <div class="dropdown-loading">
+ <loading-icon />
+ </div>
+ <dropdown-footer
+ v-if="showCreate"
+ :labels-web-url="labelsWebUrl"
+ />
+ </div>
+ <dropdown-create-label
+ v-if="showCreate"
+ />
+ </div>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
new file mode 100644
index 00000000000..47497c1de98
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button.vue
@@ -0,0 +1,78 @@
+<script>
+import { __, s__, sprintf } from '~/locale';
+
+export default {
+ props: {
+ abilityName: {
+ type: String,
+ required: true,
+ },
+ fieldName: {
+ type: String,
+ required: true,
+ },
+ updatePath: {
+ type: String,
+ required: true,
+ },
+ labelsPath: {
+ type: String,
+ required: true,
+ },
+ namespace: {
+ type: String,
+ required: true,
+ },
+ labels: {
+ type: Array,
+ required: true,
+ },
+ showExtraOptions: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ dropdownToggleText() {
+ if (this.labels.length === 0) {
+ return __('Label');
+ }
+
+ if (this.labels.length > 1) {
+ return sprintf(s__('LabelSelect|%{firstLabelName} +%{remainingLabelCount} more'), {
+ firstLabelName: this.labels[0].title,
+ remainingLabelCount: this.labels.length - 1,
+ });
+ }
+
+ return this.labels[0].title;
+ },
+ },
+};
+</script>
+
+<template>
+ <button
+ type="button"
+ ref="dropdownButton"
+ class="dropdown-menu-toggle wide js-label-select js-multiselect js-context-config-modal"
+ data-toggle="dropdown"
+ :class="{ 'js-extra-options': showExtraOptions }"
+ :data-ability-name="abilityName"
+ :data-field-name="fieldName"
+ :data-issue-update="updatePath"
+ :data-labels="labelsPath"
+ :data-namespace-path="namespace"
+ :data-show-any="showExtraOptions"
+ >
+ <span class="dropdown-toggle-text">
+ {{ dropdownToggleText }}
+ </span>
+ <i
+ aria-hidden="true"
+ class="fa fa-chevron-down"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
new file mode 100644
index 00000000000..4200d1e8473
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue
@@ -0,0 +1,84 @@
+<script>
+export default {
+ created() {
+ this.suggestedColors = gon.suggested_label_colors;
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-page-two dropdown-new-label">
+ <div class="dropdown-title">
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-back"
+ :aria-label="__('Go back')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-arrow-left"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ {{ __('Create new label') }}
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+ <div class="dropdown-content">
+ <div class="dropdown-labels-error js-label-error"></div>
+ <input
+ id="new_label_name"
+ type="text"
+ class="default-dropdown-input"
+ :placeholder="__('Name new label')"
+ />
+ <div class="suggest-colors suggest-colors-dropdown">
+ <a
+ v-for="(color, index) in suggestedColors"
+ href="#"
+ :key="index"
+ :data-color="color"
+ :style="{
+ backgroundColor: color,
+ }"
+ >
+ &nbsp;
+ </a>
+ </div>
+ <div class="dropdown-label-color-input">
+ <div class="dropdown-label-color-preview js-dropdown-label-color-preview"></div>
+ <input
+ id="new_label_color"
+ type="text"
+ class="default-dropdown-input"
+ :placeholder="__('Assign custom color like #FF0000')"
+ />
+ </div>
+ <div class="clearfix">
+ <button
+ type="button"
+ class="btn btn-primary pull-left js-new-label-btn disabled"
+ >
+ {{ __('Create') }}
+ </button>
+ <button
+ type="button"
+ class="btn btn-default pull-right js-cancel-label-btn"
+ >
+ {{ __('Cancel') }}
+ </button>
+ </div>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
new file mode 100644
index 00000000000..e951a863811
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer.vue
@@ -0,0 +1,34 @@
+<script>
+export default {
+ props: {
+ labelsWebUrl: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="dropdown-footer">
+ <ul class="dropdown-footer-list">
+ <li>
+ <a
+ href="#"
+ class="dropdown-toggle-page"
+ >
+ {{ __('Create new label') }}
+ </a>
+ </li>
+ <li>
+ <a
+ data-is-link="true"
+ class="dropdown-external-link"
+ :href="labelsWebUrl"
+ >
+ {{ __('Manage labels') }}
+ </a>
+ </li>
+ </ul>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
new file mode 100644
index 00000000000..7664acdf19c
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header.vue
@@ -0,0 +1,21 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="dropdown-title">
+ <span>{{ __('Assign labels') }}</span>
+ <button
+ type="button"
+ class="dropdown-title-button dropdown-menu-close"
+ :aria-label="__('Close')"
+ >
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-menu-close-icon"
+ data-hidden="true"
+ >
+ </i>
+ </button>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
new file mode 100644
index 00000000000..1832c3c1757
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue
@@ -0,0 +1,22 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: true,
+ },
+ label: {
+ type: Object,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <input
+ type="hidden"
+ :name="name"
+ :value="label.id"
+ />
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
new file mode 100644
index 00000000000..ae633460c95
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue
@@ -0,0 +1,27 @@
+<script>
+export default {};
+</script>
+
+<template>
+ <div class="dropdown-input">
+ <input
+ autocomplete="off"
+ class="dropdown-input-field"
+ type="search"
+ :placeholder="__('Search')"
+ />
+ <i
+ aria-hidden="true"
+ class="fa fa-search dropdown-input-search"
+ data-hidden="true"
+ >
+ </i>
+ <i
+ aria-hidden="true"
+ class="fa fa-times dropdown-input-clear js-dropdown-input-clear"
+ data-hidden="true"
+ role="button"
+ >
+ </i>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
new file mode 100644
index 00000000000..7da82e90e29
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title.vue
@@ -0,0 +1,30 @@
+<script>
+export default {
+ props: {
+ canEdit: {
+ type: Boolean,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="title hide-collapsed append-bottom-10">
+ {{ __('Labels') }}
+ <template v-if="canEdit">
+ <i
+ aria-hidden="true"
+ class="fa fa-spinner fa-spin block-loading"
+ data-hidden="true"
+ >
+ </i>
+ <button
+ type="button"
+ class="edit-link btn btn-blank pull-right js-sidebar-dropdown-toggle"
+ >
+ {{ __('Edit') }}
+ </button>
+ </template>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
new file mode 100644
index 00000000000..ba4c8fba5ec
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value.vue
@@ -0,0 +1,63 @@
+<script>
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ labelFilterBasePath: {
+ type: String,
+ required: true,
+ },
+ },
+ computed: {
+ isEmpty() {
+ return this.labels.length === 0;
+ },
+ },
+ methods: {
+ labelFilterUrl(label) {
+ return `${this.labelFilterBasePath}?label_name[]=${encodeURIComponent(label.title)}`;
+ },
+ labelStyle(label) {
+ return {
+ color: label.textColor,
+ backgroundColor: label.color,
+ };
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="hide-collapsed value issuable-show-labels">
+ <span
+ v-if="isEmpty"
+ class="text-secondary"
+ >
+ <slot>{{ __('None') }}</slot>
+ </span>
+ <a
+ v-else
+ v-for="label in labels"
+ :key="label.id"
+ :href="labelFilterUrl(label)"
+ >
+ <span
+ v-tooltip
+ class="label color-label"
+ data-placement="bottom"
+ data-container="body"
+ :style="labelStyle(label)"
+ :title="label.description"
+ >
+ {{ label.title }}
+ </span>
+ </a>
+ </div>
+</template>
diff --git a/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
new file mode 100644
index 00000000000..5cf728fe050
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue
@@ -0,0 +1,48 @@
+<script>
+import { s__, sprintf } from '~/locale';
+import tooltip from '~/vue_shared/directives/tooltip';
+
+export default {
+ directives: {
+ tooltip,
+ },
+ props: {
+ labels: {
+ type: Array,
+ required: true,
+ },
+ },
+ computed: {
+ labelsList() {
+ const labelsString = this.labels.slice(0, 5).map(label => label.title).join(', ');
+
+ if (this.labels.length > 5) {
+ return sprintf(s__('LabelSelect|%{labelsString}, and %{remainingLabelCount} more'), {
+ labelsString,
+ remainingLabelCount: this.labels.length - 5,
+ });
+ }
+
+ return labelsString;
+ },
+ },
+};
+</script>
+
+<template>
+ <div
+ v-tooltip
+ class="sidebar-collapsed-icon"
+ data-placement="left"
+ data-container="body"
+ :title="labelsList"
+ >
+ <i
+ aria-hidden="true"
+ data-hidden="true"
+ class="fa fa-tags"
+ >
+ </i>
+ <span>{{ labels.length }}</span>
+ </div>
+</template>
diff --git a/app/assets/javascripts/boards/models/label.js b/app/assets/javascripts/vue_shared/models/label.js
index 98c1ec014c4..70b9efe0c68 100644
--- a/app/assets/javascripts/boards/models/label.js
+++ b/app/assets/javascripts/vue_shared/models/label.js
@@ -1,7 +1,5 @@
-/* eslint-disable no-unused-vars, space-before-function-paren */
-
class ListLabel {
- constructor (obj) {
+ constructor(obj) {
this.id = obj.id;
this.title = obj.title;
this.type = obj.type;
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 17801ed5910..8b680c2dc52 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -196,17 +196,9 @@
@media (min-width: $screen-sm-min) {
font-size: 0;
- div {
- display: inline;
- }
-
.fa-spinner {
font-size: 12px;
}
-
- span {
- font-size: 6px;
- }
}
.ci-status-link {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 0cf67734237..4c9732c26d9 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -103,6 +103,7 @@
.issuable-show-labels {
a {
margin-bottom: 5px;
+ margin-right: 5px;
display: inline-block;
.color-label {
@@ -116,6 +117,12 @@
}
&.has-labels {
+ // this font size is a fix to
+ // prevent unintended spacing between labels
+ // which shows up when rendering markup has white-space
+ // characters present.
+ // see: https://css-tricks.com/fighting-the-space-between-inline-block-elements/#article-header-id-3
+ font-size: 0;
margin-bottom: -5px;
}
}
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 6763af4e98b..b9390450477 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -13,10 +13,20 @@
display: inline-block;
}
+ .issuable-meta {
+ .author_link {
+ display: inline-block;
+ }
+
+ .issuable-comments {
+ height: 18px;
+ }
+ }
+
.icon-merge-request-unmerged {
height: 13px;
margin-bottom: 3px;
- }
+ }
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 26e6e8688b6..3c565837383 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -723,7 +723,7 @@ ul.notes {
.line-resolve-all {
vertical-align: middle;
display: inline-block;
- padding: 5px 10px 6px;
+ padding: 6px 10px;
background-color: $gray-light;
border: 1px solid $border-color;
border-radius: $border-radius-default;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index a94726887d9..cc38608eda5 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -48,7 +48,7 @@ class Admin::GroupsController < Admin::ApplicationController
def members_update
member_params = params.permit(:user_ids, :access_level, :expires_at)
- result = Members::CreateService.new(@group, current_user, member_params.merge(limit: -1)).execute
+ result = Members::CreateService.new(current_user, member_params.merge(limit: -1)).execute(@group)
if result[:status] == :success
redirect_to [:admin, @group], notice: 'Users were successfully added.'
diff --git a/app/controllers/admin/impersonation_tokens_controller.rb b/app/controllers/admin/impersonation_tokens_controller.rb
index 7a2c7234a1e..a7b562b1d8e 100644
--- a/app/controllers/admin/impersonation_tokens_controller.rb
+++ b/app/controllers/admin/impersonation_tokens_controller.rb
@@ -9,7 +9,6 @@ class Admin::ImpersonationTokensController < Admin::ApplicationController
@impersonation_token = finder.build(impersonation_token_params)
if @impersonation_token.save
- flash[:impersonation_token] = @impersonation_token.token
redirect_to admin_user_impersonation_tokens_path, notice: "A new impersonation token has been created."
else
set_index_vars
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index e6a41202f04..7f83bd10e93 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -191,7 +191,7 @@ class ApplicationController < ActionController::Base
return unless signed_in? && session[:service_tickets]
valid = session[:service_tickets].all? do |provider, ticket|
- Gitlab::OAuth::Session.valid?(provider, ticket)
+ Gitlab::Auth::OAuth::Session.valid?(provider, ticket)
end
unless valid
@@ -215,7 +215,7 @@ class ApplicationController < ActionController::Base
if current_user && current_user.requires_ldap_check?
return unless current_user.try_obtain_ldap_lease
- unless Gitlab::LDAP::Access.allowed?(current_user)
+ unless Gitlab::Auth::LDAP::Access.allowed?(current_user)
sign_out current_user
flash[:alert] = "Access denied for your LDAP account."
redirect_to new_user_session_path
@@ -230,7 +230,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_ldap_access(&block)
- Gitlab::LDAP::Access.open { |access| yield(access) }
+ Gitlab::Auth::LDAP::Access.open { |access| yield(access) }
end
# JSON for infinite scroll via Pager object
@@ -284,7 +284,7 @@ class ApplicationController < ActionController::Base
end
def github_import_configured?
- Gitlab::OAuth::Provider.enabled?(:github)
+ Gitlab::Auth::OAuth::Provider.enabled?(:github)
end
def gitlab_import_enabled?
@@ -292,7 +292,7 @@ class ApplicationController < ActionController::Base
end
def gitlab_import_configured?
- Gitlab::OAuth::Provider.enabled?(:gitlab)
+ Gitlab::Auth::OAuth::Provider.enabled?(:gitlab)
end
def bitbucket_import_enabled?
@@ -300,7 +300,7 @@ class ApplicationController < ActionController::Base
end
def bitbucket_import_configured?
- Gitlab::OAuth::Provider.enabled?(:bitbucket)
+ Gitlab::Auth::OAuth::Provider.enabled?(:bitbucket)
end
def google_code_import_enabled?
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 337957c366d..a21e658fda1 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -77,6 +77,20 @@ module IssuableActions
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
end
+ def discussions
+ notes = issuable.notes
+ .inc_relations_for_view
+ .includes(:noteable)
+ .fresh
+
+ notes = prepare_notes_for_rendering(notes)
+ notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
+
+ discussions = Discussion.build_collection(notes, issuable)
+
+ render json: DiscussionSerializer.new(project: project, noteable: issuable, current_user: current_user).represent(discussions, context: self)
+ end
+
private
def recaptcha_check_if_spammable(should_redirect = true, &block)
diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb
index f7ba305a59f..4114ca6bf7c 100644
--- a/app/controllers/concerns/issuable_collections.rb
+++ b/app/controllers/concerns/issuable_collections.rb
@@ -17,7 +17,7 @@ module IssuableCollections
set_pagination
return if redirect_out_of_range(@total_pages)
- if params[:label_name].present?
+ if params[:label_name].present? && @project
labels_params = { project_id: @project.id, title: params[:label_name] }
@labels = LabelsFinder.new(current_user, labels_params).execute
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index c6b1e443de6..7a6a00b8e13 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -3,20 +3,31 @@ module MembershipActions
def create
create_params = params.permit(:user_ids, :access_level, :expires_at)
- result = Members::CreateService.new(membershipable, current_user, create_params).execute
-
- redirect_url = members_page_url
+ result = Members::CreateService.new(current_user, create_params).execute(membershipable)
if result[:status] == :success
- redirect_to redirect_url, notice: 'Users were successfully added.'
+ redirect_to members_page_url, notice: 'Users were successfully added.'
else
- redirect_to redirect_url, alert: result[:message]
+ redirect_to members_page_url, alert: result[:message]
+ end
+ end
+
+ def update
+ update_params = params.require(root_params_key).permit(:access_level, :expires_at)
+ member = membershipable.members_and_requesters.find(params[:id])
+ member = Members::UpdateService
+ .new(current_user, update_params)
+ .execute(member)
+ .present(current_user: current_user)
+
+ respond_to do |format|
+ format.js { render 'shared/members/update', locals: { member: member } }
end
end
def destroy
- Members::DestroyService.new(membershipable, current_user, params)
- .execute(:all)
+ member = membershipable.members_and_requesters.find(params[:id])
+ Members::DestroyService.new(current_user).execute(member)
respond_to do |format|
format.html do
@@ -36,14 +47,17 @@ module MembershipActions
end
def approve_access_request
- Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
+ access_requester = membershipable.requesters.find(params[:id])
+ Members::ApproveAccessRequestService
+ .new(current_user, params)
+ .execute(access_requester)
redirect_to members_page_url
end
def leave
- member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id)
- .execute(:all)
+ member = membershipable.members_and_requesters.find_by!(user_id: current_user.id)
+ Members::DestroyService.new(current_user).execute(member)
notice =
if member.request?
@@ -62,17 +76,43 @@ module MembershipActions
end
end
+ def resend_invite
+ member = membershipable.members.find(params[:id])
+
+ if member.invite?
+ member.resend_invite
+
+ redirect_to members_page_url, notice: 'The invitation was successfully resent.'
+ else
+ redirect_to members_page_url, alert: 'The invitation has already been accepted.'
+ end
+ end
+
protected
def membershipable
raise NotImplementedError
end
+ def root_params_key
+ case membershipable
+ when Namespace
+ :group_member
+ when Project
+ :project_member
+ else
+ raise "Unknown membershipable type: #{membershipable}!"
+ end
+ end
+
def members_page_url
- if membershipable.is_a?(Project)
+ case membershipable
+ when Namespace
+ polymorphic_url([membershipable, :members])
+ when Project
project_project_members_path(membershipable)
else
- polymorphic_url([membershipable, :members])
+ raise "Unknown membershipable type: #{membershipable}!"
end
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index e82a5650935..03ed5b5310b 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -22,7 +22,7 @@ module NotesActions
notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
notes_json[:notes] =
- if noteable.discussions_rendered_on_frontend?
+ if use_note_serializer?
note_serializer.represent(notes)
else
notes.map { |note| note_json(note) }
@@ -95,7 +95,7 @@ module NotesActions
if note.persisted?
attrs[:valid] = true
- if noteable.discussions_rendered_on_frontend?
+ if use_note_serializer?
attrs.merge!(note_serializer.represent(note))
else
attrs.merge!(
@@ -233,4 +233,14 @@ module NotesActions
the_project
end
end
+
+ def use_note_serializer?
+ return false if params['html']
+
+ if noteable.is_a?(MergeRequest)
+ cookies[:vue_mr_discussions] == 'true'
+ else
+ noteable.discussions_rendered_on_frontend?
+ end
+ end
end
diff --git a/app/controllers/groups/application_controller.rb b/app/controllers/groups/application_controller.rb
index 4a2bfc1f887..9f3bb60b4cc 100644
--- a/app/controllers/groups/application_controller.rb
+++ b/app/controllers/groups/application_controller.rb
@@ -18,10 +18,6 @@ class Groups::ApplicationController < ApplicationController
@projects ||= GroupProjectsFinder.new(group: group, current_user: current_user).execute
end
- def group_merge_requests
- @group_merge_requests = MergeRequestsFinder.new(current_user, group_id: @group.id).execute
- end
-
def authorize_admin_group!
unless can?(current_user, :admin_group, group)
return render_404
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 2c371e76313..f210434b2d7 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -27,35 +27,6 @@ class Groups::GroupMembersController < Groups::ApplicationController
@group_member = @group.group_members.new
end
- def update
- @group_member = @group.members_and_requesters.find(params[:id])
- .present(current_user: current_user)
-
- return render_403 unless can?(current_user, :update_group_member, @group_member)
-
- @group_member.update_attributes(member_params)
- end
-
- def resend_invite
- redirect_path = group_group_members_path(@group)
-
- @group_member = @group.group_members.find(params[:id])
-
- if @group_member.invite?
- @group_member.resend_invite
-
- redirect_to redirect_path, notice: 'The invitation was successfully resent.'
- else
- redirect_to redirect_path, alert: 'The invitation has already been accepted.'
- end
- end
-
- protected
-
- def member_params
- params.require(:group_member).permit(:access_level, :user_id, :expires_at)
- end
-
# MembershipActions concern
alias_method :membershipable, :group
end
diff --git a/app/controllers/groups/labels_controller.rb b/app/controllers/groups/labels_controller.rb
index f3a9e591c3e..ac1d97dc54a 100644
--- a/app/controllers/groups/labels_controller.rb
+++ b/app/controllers/groups/labels_controller.rb
@@ -14,7 +14,14 @@ class Groups::LabelsController < Groups::ApplicationController
end
format.json do
- available_labels = LabelsFinder.new(current_user, group_id: @group.id).execute
+ available_labels = LabelsFinder.new(
+ current_user,
+ group_id: @group.id,
+ only_group_labels: params[:only_group_labels],
+ include_ancestor_groups: params[:include_ancestor_groups],
+ include_descendant_groups: params[:include_descendant_groups]
+ ).execute
+
render json: LabelSerializer.new.represent_appearance(available_labels)
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 14b9d6c22bd..283c3e5f1e0 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -14,7 +14,6 @@ class GroupsController < Groups::ApplicationController
before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
- before_action :group_merge_requests, only: [:merge_requests]
before_action :event_filter, only: [:activity]
before_action :user_actions, only: [:show, :subgroups]
diff --git a/app/controllers/ide_controller.rb b/app/controllers/ide_controller.rb
deleted file mode 100644
index 1ff25a45398..00000000000
--- a/app/controllers/ide_controller.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-class IdeController < ApplicationController
- layout 'nav_only'
-
- def index
- end
-end
diff --git a/app/controllers/import/bitbucket_controller.rb b/app/controllers/import/bitbucket_controller.rb
index 13ea736688d..61d81ad8a71 100644
--- a/app/controllers/import/bitbucket_controller.rb
+++ b/app/controllers/import/bitbucket_controller.rb
@@ -71,7 +71,7 @@ class Import::BitbucketController < Import::BaseController
end
def provider
- Gitlab::OAuth::Provider.config_for('bitbucket')
+ Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def options
diff --git a/app/controllers/invites_controller.rb b/app/controllers/invites_controller.rb
index 52430ea771f..025d8270b7c 100644
--- a/app/controllers/invites_controller.rb
+++ b/app/controllers/invites_controller.rb
@@ -62,7 +62,7 @@ class InvitesController < ApplicationController
case source
when Project
project = member.source
- label = "project #{project.name_with_namespace}"
+ label = "project #{project.full_name}"
path = project_path(project)
when Group
group = member.source
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index 83c9a3f035e..8440945ab43 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -10,8 +10,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
- if Gitlab::LDAP::Config.enabled?
- Gitlab::LDAP::Config.available_servers.each do |server|
+ if Gitlab::Auth::LDAP::Config.enabled?
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
define_method server['provider_name'] do
ldap
end
@@ -31,7 +31,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
# We only find ourselves here
# if the authentication to LDAP was successful.
def ldap
- ldap_user = Gitlab::LDAP::User.new(oauth)
+ ldap_user = Gitlab::Auth::LDAP::User.new(oauth)
ldap_user.save if ldap_user.changed? # will also save new users
@user = ldap_user.gl_user
@@ -62,13 +62,13 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
redirect_to after_sign_in_path_for(current_user)
end
else
- saml_user = Gitlab::Saml::User.new(oauth)
+ saml_user = Gitlab::Auth::Saml::User.new(oauth)
saml_user.save if saml_user.changed?
@user = saml_user.gl_user
continue_login_process
end
- rescue Gitlab::OAuth::SignupDisabledError
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end
@@ -106,20 +106,20 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
redirect_to profile_account_path, notice: 'Authentication method updated'
else
- oauth_user = Gitlab::OAuth::User.new(oauth)
+ oauth_user = Gitlab::Auth::OAuth::User.new(oauth)
oauth_user.save
@user = oauth_user.gl_user
continue_login_process
end
- rescue Gitlab::OAuth::SigninDisabledForProviderError
+ rescue Gitlab::Auth::OAuth::User::SigninDisabledForProviderError
handle_disabled_provider
- rescue Gitlab::OAuth::SignupDisabledError
+ rescue Gitlab::Auth::OAuth::User::SignupDisabledError
handle_signup_error
end
def handle_service_ticket(provider, ticket)
- Gitlab::OAuth::Session.create provider, ticket
+ Gitlab::Auth::OAuth::Session.create provider, ticket
session[:service_tickets] ||= {}
session[:service_tickets][provider] = ticket
end
@@ -142,7 +142,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def handle_signup_error
- label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
+ label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
message = "Signing in using your #{label} account without a pre-existing GitLab account is not allowed."
if Gitlab::CurrentSettings.allow_signup?
@@ -171,7 +171,7 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
def handle_disabled_provider
- label = Gitlab::OAuth::Provider.label_for(oauth['provider'])
+ label = Gitlab::Auth::OAuth::Provider.label_for(oauth['provider'])
flash[:alert] = "Signing in using #{label} has been disabled"
redirect_to new_user_session_path
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index fa72f67c77e..b8ccc6e3c99 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -1,5 +1,6 @@
class Profiles::PasswordsController < Profiles::ApplicationController
skip_before_action :check_password_expiration, only: [:new, :create]
+ skip_before_action :check_two_factor_requirement, only: [:new, :create]
before_action :set_user
before_action :authorize_change_password!
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 74c25505e36..405726c017c 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -38,7 +38,7 @@ class Projects::BlobController < Projects::ApplicationController
end
format.json do
- page_title @blob.path, @ref, @project.name_with_namespace
+ page_title @blob.path, @ref, @project.full_name
show_json
end
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index cabafe26357..965cece600e 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -7,13 +7,19 @@ class Projects::BranchesController < Projects::ApplicationController
before_action :authorize_download_code!
before_action :authorize_push_code!, only: [:new, :create, :destroy, :destroy_all_merged]
- def index
- @sort = params[:sort].presence || sort_value_recently_updated
- @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
- @branches = Kaminari.paginate_array(@branches).page(params[:page])
+ # Support legacy URLs
+ before_action :redirect_for_legacy_index_sort_or_search, only: [:index]
+ def index
respond_to do |format|
format.html do
+ @sort = params[:sort].presence || sort_value_recently_updated
+ @mode = params[:state].presence || 'overview'
+ @overview_max_branches = 5
+
+ # Fetch branches for the specified mode
+ fetch_branches_by_mode
+
@refs_pipelines = @project.pipelines.latest_successful_for_refs(@branches.map(&:name))
@merged_branch_names =
repository.merged_branch_names(@branches.map(&:name))
@@ -28,7 +34,9 @@ class Projects::BranchesController < Projects::ApplicationController
end
end
format.json do
- render json: @branches.map(&:name)
+ branches = BranchesFinder.new(@repository, params).execute
+ branches = Kaminari.paginate_array(branches).page(params[:page])
+ render json: branches.map(&:name)
end
end
end
@@ -123,4 +131,27 @@ class Projects::BranchesController < Projects::ApplicationController
context: 'autodeploy'
)
end
+
+ def redirect_for_legacy_index_sort_or_search
+ # Normalize a legacy URL with redirect
+ if request.format != :json && !params[:state].presence && [:sort, :search, :page].any? { |key| params[key].presence }
+ redirect_to project_branches_filtered_path(@project, state: 'all'), notice: 'Update your bookmarked URLs as filtered/sorted branches URL has been changed.'
+ end
+ end
+
+ def fetch_branches_by_mode
+ if @mode == 'overview'
+ # overview mode
+ @active_branches, @stale_branches = BranchesFinder.new(@repository, sort: sort_value_recently_updated).execute.partition(&:active?)
+ # Here we get one more branch to indicate if there are more data we're not showing
+ @active_branches = @active_branches.first(@overview_max_branches + 1)
+ @stale_branches = @stale_branches.first(@overview_max_branches + 1)
+ @branches = @active_branches + @stale_branches
+ else
+ # active/stale/all view mode
+ @branches = BranchesFinder.new(@repository, params.merge(sort: @sort)).execute
+ @branches = @branches.select { |b| b.state.to_s == @mode } if %w[active stale].include?(@mode)
+ @branches = Kaminari.paginate_array(@branches).page(params[:page])
+ end
+ end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index 142e8b6e4bc..aeaba3a0acf 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -4,6 +4,7 @@ class Projects::ClustersController < Projects::ApplicationController
before_action :authorize_create_cluster!, only: [:new]
before_action :authorize_update_cluster!, only: [:update]
before_action :authorize_admin_cluster!, only: [:destroy]
+ before_action :update_applications_status, only: [:status]
STATUS_POLLING_INTERVAL = 10_000
@@ -114,4 +115,8 @@ class Projects::ClustersController < Projects::ApplicationController
def authorize_admin_cluster!
access_denied! unless can?(current_user, :admin_cluster, cluster)
end
+
+ def update_applications_status
+ @cluster.applications.each(&:schedule_status_update)
+ end
end
diff --git a/app/controllers/projects/commits_controller.rb b/app/controllers/projects/commits_controller.rb
index 1d910e461b1..7b7cb52d7ed 100644
--- a/app/controllers/projects/commits_controller.rb
+++ b/app/controllers/projects/commits_controller.rb
@@ -14,37 +14,31 @@ class Projects::CommitsController < Projects::ApplicationController
@merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened
.find_by(source_project: @project, source_branch: @ref, target_branch: @repository.root_ref)
- # https://gitlab.com/gitlab-org/gitaly/issues/931
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- respond_to do |format|
- format.html
- format.atom { render layout: 'xml.atom' }
+ respond_to do |format|
+ format.html
+ format.atom { render layout: 'xml.atom' }
- format.json do
- pager_json(
- 'projects/commits/_commits',
- @commits.size,
- project: @project,
- ref: @ref)
- end
+ format.json do
+ pager_json(
+ 'projects/commits/_commits',
+ @commits.size,
+ project: @project,
+ ref: @ref)
end
end
end
def signatures
- # https://gitlab.com/gitlab-org/gitaly/issues/931
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- respond_to do |format|
- format.json do
- render json: {
- signatures: @commits.select(&:has_signature?).map do |commit|
- {
- commit_sha: commit.sha,
- html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
- }
- end
- }
- end
+ respond_to do |format|
+ format.json do
+ render json: {
+ signatures: @commits.select(&:has_signature?).map do |commit|
+ {
+ commit_sha: commit.sha,
+ html: view_to_html_string('projects/commit/_signature', signature: commit.signature)
+ }
+ end
+ }
end
end
end
diff --git a/app/controllers/projects/compare_controller.rb b/app/controllers/projects/compare_controller.rb
index 3cb4eb23981..2b0c2ca97c0 100644
--- a/app/controllers/projects/compare_controller.rb
+++ b/app/controllers/projects/compare_controller.rb
@@ -17,10 +17,8 @@ class Projects::CompareController < Projects::ApplicationController
def show
apply_diff_view_cookie!
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37430
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- render
- end
+
+ render
end
def diff_for_path
diff --git a/app/controllers/projects/discussions_controller.rb b/app/controllers/projects/discussions_controller.rb
index 2e6ab7903b8..ee507009e50 100644
--- a/app/controllers/projects/discussions_controller.rb
+++ b/app/controllers/projects/discussions_controller.rb
@@ -1,4 +1,7 @@
class Projects::DiscussionsController < Projects::ApplicationController
+ include NotesHelper
+ include RendersNotes
+
before_action :check_merge_requests_available!
before_action :merge_request
before_action :discussion
@@ -7,22 +10,45 @@ class Projects::DiscussionsController < Projects::ApplicationController
def resolve
Discussions::ResolveService.new(project, current_user, merge_request: merge_request).execute(discussion)
- render json: {
- resolved_by: discussion.resolved_by.try(:name),
- discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
- }
+ render_discussion
end
def unresolve
discussion.unresolve!
+ render_discussion
+ end
+
+ private
+
+ def render_discussion
+ if serialize_notes?
+ # TODO - It is not needed to serialize notes when resolving
+ # or unresolving discussions. We should remove this behavior
+ # passing a parameter to DiscussionEntity to return an empty array
+ # for notes.
+ # Check issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/42853
+ prepare_notes_for_rendering(discussion.notes, merge_request)
+ render_json_with_discussions_serializer
+ else
+ render_json_with_html
+ end
+ end
+
+ def render_json_with_discussions_serializer
+ render json:
+ DiscussionSerializer.new(project: project, noteable: discussion.noteable, current_user: current_user)
+ .represent(discussion, context: self)
+ end
+
+ # Legacy method used to render discussions notes when not using Vue on views.
+ def render_json_with_html
render json: {
+ resolved_by: discussion.resolved_by.try(:name),
discussion_headline_html: view_to_html_string('discussions/_headline', discussion: discussion)
}
end
- private
-
def merge_request
@merge_request ||= MergeRequestsFinder.new(current_user, project_id: @project.id).find_by!(iid: params[:merge_request_id])
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 73806454525..b14939c4216 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -60,20 +60,6 @@ class Projects::IssuesController < Projects::ApplicationController
respond_with(@issue)
end
- def discussions
- notes = @issue.notes
- .inc_relations_for_view
- .includes(:noteable)
- .fresh
-
- notes = prepare_notes_for_rendering(notes)
- notes = notes.reject { |n| n.cross_reference_not_visible_for?(current_user) }
-
- discussions = Discussion.build_collection(notes, @issue)
-
- render json: DiscussionSerializer.new(project: @project, noteable: @issue, current_user: current_user).represent(discussions)
- end
-
def create
create_params = issue_params.merge(spammable_params).merge(
merge_request_to_resolve_discussions_of: params[:merge_request_to_resolve_discussions_of],
diff --git a/app/controllers/projects/network_controller.rb b/app/controllers/projects/network_controller.rb
index 3b10a93e97f..35fec229db7 100644
--- a/app/controllers/projects/network_controller.rb
+++ b/app/controllers/projects/network_controller.rb
@@ -9,25 +9,22 @@ class Projects::NetworkController < Projects::ApplicationController
before_action :assign_commit
def show
- # n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/37602
- Gitlab::GitalyClient.allow_n_plus_1_calls do
- @url = project_network_path(@project, @ref, @options.merge(format: :json))
- @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
-
- respond_to do |format|
- format.html do
- if @options[:extended_sha1] && !@commit
- flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
- end
- end
+ @url = project_network_path(@project, @ref, @options.merge(format: :json))
+ @commit_url = project_commit_path(@project, 'ae45ca32').gsub("ae45ca32", "%s")
- format.json do
- @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ respond_to do |format|
+ format.html do
+ if @options[:extended_sha1] && !@commit
+ flash.now[:alert] = "Git revision '#{@options[:extended_sha1]}' does not exist."
end
end
- render
+ format.json do
+ @graph = Network::Graph.new(project, @ref, @commit, @options[:filter_ref])
+ end
end
+
+ render
end
def assign_commit
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 4f8978c93c3..dd41b9648e8 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,5 +1,6 @@
class Projects::NotesController < Projects::ApplicationController
include NotesActions
+ include NotesHelper
include ToggleAwardEmoji
before_action :whitelist_query_limiting, only: [:create]
@@ -38,10 +39,14 @@ class Projects::NotesController < Projects::ApplicationController
discussion = note.discussion
- render json: {
- resolved_by: note.resolved_by.try(:name),
- discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
- }
+ if serialize_notes?
+ render_json_with_notes_serializer
+ else
+ render json: {
+ resolved_by: note.resolved_by.try(:name),
+ discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+ }
+ end
end
def unresolve
@@ -51,16 +56,27 @@ class Projects::NotesController < Projects::ApplicationController
discussion = note.discussion
- render json: {
- discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
- }
+ if serialize_notes?
+ render_json_with_notes_serializer
+ else
+ render json: {
+ discussion_headline_html: (view_to_html_string('discussions/_headline', discussion: discussion) if discussion)
+ }
+ end
end
private
+ def render_json_with_notes_serializer
+ Notes::RenderService.new(current_user).execute([note], project)
+
+ render json: note_serializer.represent(note)
+ end
+
def note
@note ||= @project.notes.find(params[:id])
end
+
alias_method :awardable, :note
def finder_params
diff --git a/app/controllers/projects/pages_domains_controller.rb b/app/controllers/projects/pages_domains_controller.rb
index b71f1e5fef4..4856be61e88 100644
--- a/app/controllers/projects/pages_domains_controller.rb
+++ b/app/controllers/projects/pages_domains_controller.rb
@@ -3,7 +3,7 @@ class Projects::PagesDomainsController < Projects::ApplicationController
before_action :require_pages_enabled!
before_action :authorize_update_pages!, except: [:show]
- before_action :domain, only: [:show, :destroy, :verify]
+ before_action :domain, except: [:new, :create]
def show
end
@@ -24,8 +24,11 @@ class Projects::PagesDomainsController < Projects::ApplicationController
redirect_to project_pages_domain_path(@project, @domain)
end
+ def edit
+ end
+
def create
- @domain = @project.pages_domains.create(pages_domain_params)
+ @domain = @project.pages_domains.create(create_params)
if @domain.valid?
redirect_to project_pages_domain_path(@project, @domain)
@@ -34,6 +37,16 @@ class Projects::PagesDomainsController < Projects::ApplicationController
end
end
+ def update
+ if @domain.update(update_params)
+ redirect_to project_pages_path(@project),
+ status: 302,
+ notice: 'Domain was updated'
+ else
+ render 'edit'
+ end
+ end
+
def destroy
@domain.destroy
@@ -49,12 +62,12 @@ class Projects::PagesDomainsController < Projects::ApplicationController
private
- def pages_domain_params
- params.require(:pages_domain).permit(
- :certificate,
- :key,
- :domain
- )
+ def create_params
+ params.require(:pages_domain).permit(:key, :certificate, :domain)
+ end
+
+ def update_params
+ params.require(:pages_domain).permit(:key, :certificate)
end
def domain
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index d7372beb9d3..e9b4679f94c 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -26,29 +26,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@project_member = @project.project_members.new
end
- def update
- @project_member = @project.members_and_requesters.find(params[:id])
- .present(current_user: current_user)
-
- return render_403 unless can?(current_user, :update_project_member, @project_member)
-
- @project_member.update_attributes(member_params)
- end
-
- def resend_invite
- redirect_path = project_project_members_path(@project)
-
- @project_member = @project.project_members.find(params[:id])
-
- if @project_member.invite?
- @project_member.resend_invite
-
- redirect_to redirect_path, notice: 'The invitation was successfully resent.'
- else
- redirect_to redirect_path, alert: 'The invitation has already been accepted.'
- end
- end
-
def import
@projects = current_user.authorized_projects.order_id_desc
end
@@ -67,12 +44,6 @@ class Projects::ProjectMembersController < Projects::ApplicationController
notice: notice)
end
- protected
-
- def member_params
- params.require(:project_member).permit(:user_id, :access_level, :expires_at)
- end
-
# MembershipActions concern
alias_method :membershipable, :project
end
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index f752a46f828..ee9b5458282 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -36,7 +36,7 @@ class Projects::TreeController < Projects::ApplicationController
end
format.json do
- page_title @path.presence || _("Files"), @ref, @project.name_with_namespace
+ page_title @path.presence || _("Files"), @ref, @project.full_name
# n+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/38261
Gitlab::GitalyClient.allow_n_plus_1_calls do
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 913689a1e74..ee197c75764 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -41,11 +41,11 @@ class ProjectsController < Projects::ApplicationController
cookies[:issue_board_welcome_hidden] = { path: project_path(@project), value: nil, expires: Time.at(0) }
redirect_to(
- project_path(@project),
+ project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
)
else
- render 'new', locals: { active_tab: ('import' if project_params[:import_url].present?) }
+ render 'new', locals: { active_tab: active_new_project_tab }
end
end
@@ -103,7 +103,7 @@ class ProjectsController < Projects::ApplicationController
def show
if @project.import_in_progress?
- redirect_to project_import_path(@project)
+ redirect_to project_import_path(@project, custom_import_params)
return
end
@@ -130,7 +130,7 @@ class ProjectsController < Projects::ApplicationController
return access_denied! unless can?(current_user, :remove_project, @project)
::Projects::DestroyService.new(@project, current_user, {}).async_execute
- flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.name_with_namespace }
+ flash[:notice] = _("Project '%{project_name}' is in the process of being deleted.") % { project_name: @project.full_name }
redirect_to dashboard_projects_path, status: 302
rescue Projects::DestroyService::DestroyError => ex
@@ -359,6 +359,14 @@ class ProjectsController < Projects::ApplicationController
]
end
+ def custom_import_params
+ {}
+ end
+
+ def active_new_project_tab
+ project_params[:import_url].present? ? 'import' : 'blank'
+ end
+
def repo_exists?
project.repository_exists? && !project.empty_repo?
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index c73306a6b66..f3a4aa849c7 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -16,7 +16,7 @@ class SessionsController < Devise::SessionsController
def new
set_minimum_password_length
- @ldap_servers = Gitlab::LDAP::Config.available_servers
+ @ldap_servers = Gitlab::Auth::LDAP::Config.available_servers
super
end
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index 852eac3647d..8bb1366867c 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -1,5 +1,5 @@
class BranchesFinder
- def initialize(repository, params)
+ def initialize(repository, params = {})
@repository = repository
@params = params
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 9dd6634b38f..b2d4f9938ff 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -19,6 +19,10 @@
# non_archived: boolean
# iids: integer[]
# my_reaction_emoji: string
+# created_after: datetime
+# created_before: datetime
+# updated_after: datetime
+# updated_before: datetime
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -79,6 +83,7 @@ class IssuableFinder
def filter_items(items)
items = by_scope(items)
items = by_created_at(items)
+ items = by_updated_at(items)
items = by_state(items)
items = by_group(items)
items = by_search(items)
@@ -283,6 +288,13 @@ class IssuableFinder
end
end
+ def by_updated_at(items)
+ items = items.updated_after(params[:updated_after]) if params[:updated_after].present?
+ items = items.updated_before(params[:updated_before]) if params[:updated_before].present?
+
+ items
+ end
+
def by_state(items)
case params[:state].to_s
when 'closed'
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index d65c620e75a..2a27ff0e386 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -17,6 +17,10 @@
# my_reaction_emoji: string
# public_only: boolean
# due_date: date or '0', '', 'overdue', 'week', or 'month'
+# created_after: datetime
+# created_before: datetime
+# updated_after: datetime
+# updated_before: datetime
#
class IssuesFinder < IssuableFinder
CONFIDENTIAL_ACCESS_LEVEL = Gitlab::Access::REPORTER
diff --git a/app/finders/labels_finder.rb b/app/finders/labels_finder.rb
index f013e177c5b..780c0fdb03e 100644
--- a/app/finders/labels_finder.rb
+++ b/app/finders/labels_finder.rb
@@ -39,7 +39,7 @@ class LabelsFinder < UnionFinder
end
end
elsif only_group_labels?
- label_ids << Label.where(group_id: group.id)
+ label_ids << Label.where(group_id: group_ids)
else
label_ids << Label.where(group_id: projects.group_ids)
label_ids << Label.where(project_id: projects.select(:id))
@@ -59,13 +59,22 @@ class LabelsFinder < UnionFinder
items.where(title: title)
end
- def group
- strong_memoize(:group) do
- group = Group.find(params[:group_id])
- authorized_to_read_labels?(group) && group
+ def group_ids
+ strong_memoize(:group_ids) do
+ groups_user_can_read_labels(groups_to_include).map(&:id)
end
end
+ def groups_to_include
+ group = Group.find(params[:group_id])
+ groups = [group]
+
+ groups += group.ancestors if params[:include_ancestor_groups].present?
+ groups += group.descendants if params[:include_descendant_groups].present?
+
+ groups
+ end
+
def group?
params[:group_id].present?
end
@@ -120,4 +129,10 @@ class LabelsFinder < UnionFinder
Ability.allowed?(current_user, :read_label, label_parent)
end
+
+ def groups_user_can_read_labels(groups)
+ DeclarativePolicy.user_scope do
+ groups.select { |group| authorized_to_read_labels?(group) }
+ end
+ end
end
diff --git a/app/finders/merge_requests_finder.rb b/app/finders/merge_requests_finder.rb
index d0687d28c21..64dc1e6af0f 100644
--- a/app/finders/merge_requests_finder.rb
+++ b/app/finders/merge_requests_finder.rb
@@ -17,14 +17,46 @@
# sort: string
# non_archived: boolean
# my_reaction_emoji: string
+# source_branch: string
+# target_branch: string
+# created_after: datetime
+# created_before: datetime
+# updated_after: datetime
+# updated_before: datetime
#
class MergeRequestsFinder < IssuableFinder
def klass
MergeRequest
end
+ def filter_items(_items)
+ items = by_source_branch(super)
+
+ by_target_branch(items)
+ end
+
private
+ def source_branch
+ @source_branch ||= params[:source_branch].presence
+ end
+
+ def by_source_branch(items)
+ return items unless source_branch
+
+ items.where(source_branch: source_branch)
+ end
+
+ def target_branch
+ @target_branch ||= params[:target_branch].presence
+ end
+
+ def by_target_branch(items)
+ return items unless target_branch
+
+ items.where(target_branch: target_branch)
+ end
+
def item_project_ids(items)
items&.reorder(nil)&.select(:target_project_id)
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 33ee1e975b9..35f4ff2f62f 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -48,11 +48,23 @@ class NotesFinder
def init_collection
if target
notes_on_target
+ elsif target_type
+ notes_of_target_type
else
notes_of_any_type
end
end
+ def notes_of_target_type
+ notes = notes_for_type(target_type)
+
+ search(notes)
+ end
+
+ def target_type
+ @params[:target_type]
+ end
+
def notes_of_any_type
types = %w(commit issue merge_request snippet)
note_relations = types.map { |t| notes_for_type(t) }
diff --git a/app/finders/snippets_finder.rb b/app/finders/snippets_finder.rb
index a73c573736e..d498a2d6d11 100644
--- a/app/finders/snippets_finder.rb
+++ b/app/finders/snippets_finder.rb
@@ -58,11 +58,37 @@ class SnippetsFinder < UnionFinder
.public_or_visible_to_user(current_user)
end
+ # Returns a collection of projects that is either public or visible to the
+ # logged in user.
+ #
+ # A caller must pass in a block to modify individual parts of
+ # the query, e.g. to apply .with_feature_available_for_user on top of it.
+ # This is useful for performance as we can stick those additional filters
+ # at the bottom of e.g. the UNION.
+ def projects_for_user
+ return yield(Project.public_to_user) unless current_user
+
+ # If the current_user is allowed to see all projects,
+ # we can shortcut and just return.
+ return yield(Project.all) if current_user.full_private_access?
+
+ authorized_projects = yield(Project.where('EXISTS (?)', current_user.authorizations_for_projects))
+
+ levels = Gitlab::VisibilityLevel.levels_for_user(current_user)
+ visible_projects = yield(Project.where(visibility_level: levels))
+
+ # We use a UNION here instead of OR clauses since this results in better
+ # performance.
+ union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
+
+ Project.from("(#{union.to_sql}) AS #{Project.table_name}")
+ end
+
def feature_available_projects
# Don't return any project related snippets if the user cannot read cross project
return table[:id].eq(nil) unless Ability.allowed?(current_user, :read_cross_project)
- projects = Project.public_or_visible_to_user(current_user, use_where_in: false) do |part|
+ projects = projects_for_user do |part|
part.with_feature_available_for_user(:snippets, current_user)
end.select(:id)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index edb17843002..150f4c7688b 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -110,10 +110,6 @@ class TodosFinder
ids
end
- def projects(items)
- ProjectsFinder.new(current_user: current_user, project_ids_relation: project_ids(items)).execute
- end
-
def type?
type.present? && %w(Issue MergeRequest).include?(type)
end
@@ -152,13 +148,12 @@ class TodosFinder
def by_project(items)
if project?
- items = items.where(project: project)
+ items.where(project: project)
else
- item_projects = projects(items)
- items = items.merge(item_projects).joins(:project)
- end
+ projects = Project.public_or_visible_to_user(current_user)
- items
+ items.joins(:project).merge(projects)
+ end
end
def by_state(items)
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 475341cf9b1..af9c8bf1bd3 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -320,10 +320,6 @@ module ApplicationHelper
cookies["sidebar_collapsed"] == "true"
end
- def show_new_ide?
- cookies["new_repo"] == "true" && body_data_page != 'projects:show'
- end
-
def locale_path
asset_path("locale/#{Gitlab::I18n.locale}/app.js")
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index ab68ecad2ba..4c4d7cca8a5 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -77,7 +77,7 @@ module ApplicationSettingsHelper
label_tag(checkbox_name, class: css_class) do
check_box_tag(checkbox_name, source, !disabled,
- autocomplete: 'off') + Gitlab::OAuth::Provider.label_for(source)
+ autocomplete: 'off') + Gitlab::Auth::OAuth::Provider.label_for(source)
end
end
end
diff --git a/app/helpers/auth_helper.rb b/app/helpers/auth_helper.rb
index f909f664034..c109954f3a3 100644
--- a/app/helpers/auth_helper.rb
+++ b/app/helpers/auth_helper.rb
@@ -3,7 +3,7 @@ module AuthHelper
FORM_BASED_PROVIDERS = [/\Aldap/, 'crowd'].freeze
def ldap_enabled?
- Gitlab::LDAP::Config.enabled?
+ Gitlab::Auth::LDAP::Config.enabled?
end
def omniauth_enabled?
@@ -15,11 +15,11 @@ module AuthHelper
end
def auth_providers
- Gitlab::OAuth::Provider.providers
+ Gitlab::Auth::OAuth::Provider.providers
end
def label_for_provider(name)
- Gitlab::OAuth::Provider.label_for(name)
+ Gitlab::Auth::OAuth::Provider.label_for(name)
end
def form_based_provider?(name)
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index a6e1de6ffdc..5ff09b23a78 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -12,75 +12,28 @@ module BlobHelper
def edit_blob_path(project = @project, ref = @ref, path = @path, options = {})
project_edit_blob_path(project,
- tree_join(ref, path),
- options[:link_opts])
- end
-
- def edit_blob_link(project = @project, ref = @ref, path = @path, options = {})
- blob = options.delete(:blob)
- blob ||= project.repository.blob_at(ref, path) rescue nil
-
- return unless blob && blob.readable_text?
-
- common_classes = "btn js-edit-blob #{options[:extra_class]}"
-
- if !on_top_of_branch?(project, ref)
- 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_blob_path(project, ref, path, options), class: "#{common_classes} btn-sm"
- elsif current_user && can?(current_user, :fork_project, project)
- continue_params = {
- to: edit_blob_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 'Edit',
- class: "#{common_classes} js-edit-blob-link-fork-toggler",
- data: { action: 'edit', fork_path: fork_path }
- end
+ tree_join(ref, path),
+ options[:link_opts])
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
- "#{_('Web IDE')}"
- 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?
+ def edit_blob_button(project = @project, ref = @ref, path = @path, options = {})
+ return unless blob = readable_blob(options, path, project, ref)
- common_classes = "btn js-edit-ide #{options[:extra_class]}"
+ common_classes = "btn js-edit-blob #{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
+ edit_button_tag(blob,
+ common_classes,
+ _('Edit'),
+ edit_blob_path(project, ref, path, options),
+ project,
+ ref)
end
- def modify_file_link(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
+ def modify_file_button(project = @project, ref = @ref, path = @path, label:, action:, btn_class:, modal_type:)
return unless current_user
blob = project.repository.blob_at(ref, path) rescue nil
@@ -96,21 +49,12 @@ module BlobHelper
elsif can_modify_blob?(blob, project, ref)
button_tag label, class: "#{common_classes}", 'data-target' => "#modal-#{modal_type}-blob", 'data-toggle' => 'modal'
elsif can?(current_user, :fork_project, project)
- continue_params = {
- to: request.fullpath,
- notice: edit_in_new_fork_notice + " Try to #{action} this 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)
-
- button_tag label,
- class: "#{common_classes} js-edit-blob-link-fork-toggler",
- data: { action: action, fork_path: fork_path }
+ edit_fork_button_tag(common_classes, project, label, edit_modify_file_fork_params(action), action)
end
end
def replace_blob_link(project = @project, ref = @ref, path = @path)
- modify_file_link(
+ modify_file_button(
project,
ref,
path,
@@ -122,7 +66,7 @@ module BlobHelper
end
def delete_blob_link(project = @project, ref = @ref, path = @path)
- modify_file_link(
+ modify_file_button(
project,
ref,
path,
@@ -332,4 +276,55 @@ module BlobHelper
options
end
+
+ def readable_blob(options, path, project, ref)
+ blob = options.delete(:blob)
+ blob ||= project.repository.blob_at(ref, path) rescue nil
+
+ blob if blob&.readable_text?
+ end
+
+ def edit_blob_fork_params(path)
+ {
+ to: path,
+ notice: edit_in_new_fork_notice,
+ notice_now: edit_in_new_fork_notice_now
+ }
+ end
+
+ def edit_modify_file_fork_params(action)
+ {
+ to: request.fullpath,
+ notice: edit_in_new_fork_notice_action(action),
+ notice_now: edit_in_new_fork_notice_now
+ }
+ end
+
+ def edit_fork_button_tag(common_classes, project, label, params, action = 'edit')
+ fork_path = project_forks_path(project, namespace_key: current_user.namespace.id, continue: params)
+
+ button_tag label,
+ class: "#{common_classes} js-edit-blob-link-fork-toggler",
+ data: { action: action, fork_path: fork_path }
+ end
+
+ def edit_disabled_button_tag(button_text, common_classes)
+ button_tag(button_text, class: "#{common_classes} disabled has-tooltip", title: _('You can only edit files when you are on a branch'), data: { container: 'body' })
+ end
+
+ def edit_link_tag(link_text, edit_path, common_classes)
+ link_to link_text, edit_path, class: "#{common_classes} btn-sm"
+ end
+
+ def edit_button_tag(blob, common_classes, text, edit_path, project, ref)
+ if !on_top_of_branch?(project, ref)
+ edit_disabled_button_tag(text, common_classes)
+ # This condition only applies to users who are logged in
+ # Web IDE (Beta) requires the user to have this feature enabled
+ elsif !current_user || (current_user && can_modify_blob?(blob, project, ref))
+ edit_link_tag(text, edit_path, common_classes)
+ elsif current_user && can?(current_user, :fork_project, project)
+ edit_fork_button_tag(common_classes, project, text, edit_blob_fork_params(edit_path))
+ end
+ end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 00b9a0e00eb..07b1fc3d7cf 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -1,15 +1,4 @@
module BranchesHelper
- def filter_branches_path(options = {})
- exist_opts = {
- search: params[:search],
- sort: params[:sort]
- }
-
- options = exist_opts.merge(options)
-
- project_branches_path(@project, @id, options)
- end
-
def project_branches
options_for_select(@project.repository.branch_names, @project.default_branch)
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index 5fbaa17c40e..7910de73c52 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -19,6 +19,20 @@ module GroupsHelper
can?(current_user, :change_share_with_group_lock, group)
end
+ def group_issues_count(state:)
+ IssuesFinder
+ .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
+ .execute
+ .count
+ end
+
+ def group_merge_requests_count(state:)
+ MergeRequestsFinder
+ .new(current_user, group_id: @group.id, state: state, non_archived: true, include_subgroups: true)
+ .execute
+ .count
+ end
+
def group_icon(group, options = {})
img_path = group_icon_url(group, options)
image_tag img_path, options
@@ -77,10 +91,6 @@ module GroupsHelper
end
end
- def group_issues(group)
- IssuesFinder.new(current_user, group_id: group.id).execute
- end
-
def remove_group_message(group)
_("You are going to remove %{group_name}. Removed groups CANNOT be restored! Are you ABSOLUTELY sure?") %
{ group_name: group.name }
diff --git a/app/helpers/import_helper.rb b/app/helpers/import_helper.rb
index a18ebfb6030..b484a868f92 100644
--- a/app/helpers/import_helper.rb
+++ b/app/helpers/import_helper.rb
@@ -1,19 +1,45 @@
module ImportHelper
+ def has_ci_cd_only_params?
+ false
+ end
+
def import_project_target(owner, name)
namespace = current_user.can_create_group? ? owner : current_user.namespace_path
"#{namespace}/#{name}"
end
- def provider_project_link(provider, path_with_namespace)
- url = __send__("#{provider}_project_url", path_with_namespace) # rubocop:disable GitlabSecurity/PublicSend
+ def provider_project_link(provider, full_path)
+ url = __send__("#{provider}_project_url", full_path) # rubocop:disable GitlabSecurity/PublicSend
+
+ link_to full_path, url, target: '_blank', rel: 'noopener noreferrer'
+ end
+
+ def import_will_timeout_message(_ci_cd_only)
+ timeout = time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)
+ _('The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination.') % { timeout: timeout }
+ end
+
+ def import_svn_message(_ci_cd_only)
+ svn_link = link_to _('this document'), help_page_path('user/project/import/svn')
+ _('To import an SVN repository, check out %{svn_link}.').html_safe % { svn_link: svn_link }
+ end
+
+ def import_in_progress_title
+ if @project.forked?
+ _('Forking in progress')
+ else
+ _('Import in progress')
+ end
+ end
- link_to path_with_namespace, url, target: '_blank', rel: 'noopener noreferrer'
+ def import_wait_and_refresh_message
+ _('Please wait while we import the repository for you. Refresh at will.')
end
private
- def github_project_url(path_with_namespace)
- "#{github_root_url}/#{path_with_namespace}"
+ def github_project_url(full_path)
+ "#{github_root_url}/#{full_path}"
end
def github_root_url
@@ -23,7 +49,7 @@ module ImportHelper
@github_url = provider.fetch('url', 'https://github.com') if provider
end
- def gitea_project_url(path_with_namespace)
- "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{path_with_namespace}"
+ def gitea_project_url(full_path)
+ "#{@gitea_host_url.sub(%r{/+\z}, '')}/#{full_path}"
end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 7cd84fe69c9..f6ddb6d4cfe 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -99,7 +99,7 @@ module IssuablesHelper
project = Project.find_by(id: project_id)
if project
- project.name_with_namespace
+ project.full_name
else
default_label
end
@@ -234,7 +234,7 @@ module IssuablesHelper
data.merge!(updated_at_by(issuable))
- data.to_json
+ data
end
def updated_at_by(issuable)
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index c1c19062c91..b2c641a5dbd 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -1,4 +1,5 @@
module LabelsHelper
+ extend self
include ActionView::Helpers::TagHelper
def show_label_issuables_link?(label, issuables_type, current_user: nil, project: nil)
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index c219aa3d6a9..a70e73a6da9 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -11,7 +11,7 @@ module NotesHelper
end
def note_supports_quick_actions?(note)
- Notes::QuickActionsService.supported?(note, current_user)
+ Notes::QuickActionsService.supported?(note)
end
def noteable_json(noteable)
@@ -151,7 +151,38 @@ module NotesHelper
}
end
+ def notes_data(issuable)
+ discussions_path =
+ if issuable.is_a?(Issue)
+ discussions_project_issue_path(@project, issuable, format: :json)
+ else
+ discussions_project_merge_request_path(@project, issuable, format: :json)
+ end
+
+ {
+ discussionsPath: discussions_path,
+ registerPath: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
+ newSessionPath: new_session_path(:user, redirect_to_referer: 'yes'),
+ markdownDocsPath: help_page_path('user/markdown'),
+ quickActionsDocsPath: help_page_path('user/project/quick_actions'),
+ closePath: close_issuable_path(issuable),
+ reopenPath: reopen_issuable_path(issuable),
+ notesPath: notes_url,
+ totalNotes: issuable.discussions.length,
+ lastFetchedAt: Time.now
+
+ }.to_json
+ end
+
def discussion_resolved_intro(discussion)
discussion.resolved_by_push? ? 'Automatically resolved' : 'Resolved'
end
+
+ def has_vue_discussions_cookie?
+ cookies[:vue_mr_discussions] == 'true'
+ end
+
+ def serialize_notes?
+ has_vue_discussions_cookie? && !params['html']
+ end
end
diff --git a/app/helpers/profiles_helper.rb b/app/helpers/profiles_helper.rb
index 5a4fda0724c..e7aa92e6e5c 100644
--- a/app/helpers/profiles_helper.rb
+++ b/app/helpers/profiles_helper.rb
@@ -3,7 +3,7 @@ module ProfilesHelper
user_synced_attributes_metadata = current_user.user_synced_attributes_metadata
if user_synced_attributes_metadata&.synced?(attribute)
if user_synced_attributes_metadata.provider
- Gitlab::OAuth::Provider.label_for(user_synced_attributes_metadata.provider)
+ Gitlab::Auth::OAuth::Provider.label_for(user_synced_attributes_metadata.provider)
else
'LDAP'
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index cc1c69a1999..da9fe734f1c 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -97,13 +97,13 @@ module ProjectsHelper
end
def remove_project_message(project)
- _("You are going to remove %{project_name_with_namespace}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
- { project_name_with_namespace: project.name_with_namespace }
+ _("You are going to remove %{project_full_name}. Removed project CANNOT be restored! Are you ABSOLUTELY sure?") %
+ { project_full_name: project.full_name }
end
def transfer_project_message(project)
- _("You are going to transfer %{project_name_with_namespace} to another owner. Are you ABSOLUTELY sure?") %
- { project_name_with_namespace: project.name_with_namespace }
+ _("You are going to transfer %{project_full_name} to another owner. Are you ABSOLUTELY sure?") %
+ { project_full_name: project.full_name }
end
def remove_fork_project_message(project)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index e6a6496871a..761c1252fc8 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -110,7 +110,7 @@ module SearchHelper
category: "Projects",
id: p.id,
value: "#{search_result_sanitize(p.name)}",
- label: "#{search_result_sanitize(p.name_with_namespace)}",
+ label: "#{search_result_sanitize(p.full_name)}",
url: project_path(p)
}
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index ddb48371c79..f7620e0b6b8 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -114,7 +114,7 @@ module TodosHelper
projects = current_user.authorized_projects.sorted_by_activity.non_archived.with_route
projects = projects.map do |project|
- { id: project.id, text: project.name_with_namespace }
+ { id: project.id, text: project.full_name }
end
projects.unshift({ id: '', text: 'Any Project' }).to_json
diff --git a/app/helpers/tree_helper.rb b/app/helpers/tree_helper.rb
index f5733b4b57c..f6a6d9bebde 100644
--- a/app/helpers/tree_helper.rb
+++ b/app/helpers/tree_helper.rb
@@ -83,6 +83,10 @@ module TreeHelper
" A fork of this project has been created that you can make changes in, so you can submit a merge request."
end
+ def edit_in_new_fork_notice_action(action)
+ edit_in_new_fork_notice + " Try to #{action} this file again."
+ end
+
def commit_in_fork_help
"A new branch will be created in your fork and a new merge request will be started."
end
diff --git a/app/helpers/u2f_helper.rb b/app/helpers/u2f_helper.rb
deleted file mode 100644
index 81bfe5d4eeb..00000000000
--- a/app/helpers/u2f_helper.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-module U2fHelper
- def inject_u2f_api?
- ((browser.chrome? && browser.version.to_i >= 41) || (browser.opera? && browser.version.to_i >= 40)) && !browser.device.mobile?
- end
-end
diff --git a/app/models/badge.rb b/app/models/badge.rb
new file mode 100644
index 00000000000..f7e10c2ebfc
--- /dev/null
+++ b/app/models/badge.rb
@@ -0,0 +1,51 @@
+class Badge < ActiveRecord::Base
+ # This structure sets the placeholders that the urls
+ # can have. This hash also sets which action to ask when
+ # the placeholder is found.
+ PLACEHOLDERS = {
+ 'project_path' => :full_path,
+ 'project_id' => :id,
+ 'default_branch' => :default_branch,
+ 'commit_sha' => ->(project) { project.commit&.sha }
+ }.freeze
+
+ # This regex is built dynamically using the keys from the PLACEHOLDER struct.
+ # So, we can easily add new placeholder just by modifying the PLACEHOLDER hash.
+ # This regex will build the new PLACEHOLDER_REGEX with the new information
+ PLACEHOLDERS_REGEX = /(#{PLACEHOLDERS.keys.join('|')})/.freeze
+
+ default_scope { order_created_at_asc }
+
+ scope :order_created_at_asc, -> { reorder(created_at: :asc) }
+
+ validates :link_url, :image_url, url_placeholder: { protocols: %w(http https), placeholder_regex: PLACEHOLDERS_REGEX }
+ validates :type, presence: true
+
+ def rendered_link_url(project = nil)
+ build_rendered_url(link_url, project)
+ end
+
+ def rendered_image_url(project = nil)
+ build_rendered_url(image_url, project)
+ end
+
+ private
+
+ def build_rendered_url(url, project = nil)
+ return url unless valid? && project
+
+ Gitlab::StringPlaceholderReplacer.replace_string_placeholders(url, PLACEHOLDERS_REGEX) do |arg|
+ replace_placeholder_action(PLACEHOLDERS[arg], project)
+ end
+ end
+
+ # The action param represents the :symbol or Proc to call in order
+ # to retrieve the return value from the project.
+ # This method checks if it is a Proc and use the call method, and if it is
+ # a symbol just send the action
+ def replace_placeholder_action(action, project)
+ return unless project
+
+ action.is_a?(Proc) ? action.call(project) : project.public_send(action) # rubocop:disable GitlabSecurity/PublicSend
+ end
+end
diff --git a/app/models/badges/group_badge.rb b/app/models/badges/group_badge.rb
new file mode 100644
index 00000000000..f4b2bdecdcc
--- /dev/null
+++ b/app/models/badges/group_badge.rb
@@ -0,0 +1,5 @@
+class GroupBadge < Badge
+ belongs_to :group
+
+ validates :group, presence: true
+end
diff --git a/app/models/badges/project_badge.rb b/app/models/badges/project_badge.rb
new file mode 100644
index 00000000000..3945b376052
--- /dev/null
+++ b/app/models/badges/project_badge.rb
@@ -0,0 +1,15 @@
+class ProjectBadge < Badge
+ belongs_to :project
+
+ validates :project, presence: true
+
+ def rendered_link_url(project = nil)
+ project ||= self.project
+ super
+ end
+
+ def rendered_image_url(project = nil)
+ project ||= self.project
+ super
+ end
+end
diff --git a/app/models/chat_name.rb b/app/models/chat_name.rb
index f321db75eeb..fbd0f123341 100644
--- a/app/models/chat_name.rb
+++ b/app/models/chat_name.rb
@@ -1,4 +1,6 @@
class ChatName < ActiveRecord::Base
+ LAST_USED_AT_INTERVAL = 1.hour
+
belongs_to :service
belongs_to :user
@@ -9,4 +11,23 @@ class ChatName < ActiveRecord::Base
validates :user_id, uniqueness: { scope: [:service_id] }
validates :chat_id, uniqueness: { scope: [:service_id, :team_id] }
+
+ # Updates the "last_used_timestamp" but only if it wasn't already updated
+ # recently.
+ #
+ # The throttling this method uses is put in place to ensure that high chat
+ # traffic doesn't result in many UPDATE queries being performed.
+ def update_last_used_at
+ return unless update_last_used_at?
+
+ obtained = Gitlab::ExclusiveLease
+ .new("chat_name/last_used_at/#{id}", timeout: LAST_USED_AT_INTERVAL.to_i)
+ .try_obtain
+
+ touch(:last_used_at) if obtained
+ end
+
+ def update_last_used_at?
+ last_used_at.nil? || last_used_at > LAST_USED_AT_INTERVAL.ago
+ end
end
diff --git a/app/models/ci/group_variable.rb b/app/models/ci/group_variable.rb
index afeae69ba39..1dd0e050ba9 100644
--- a/app/models/ci/group_variable.rb
+++ b/app/models/ci/group_variable.rb
@@ -6,7 +6,10 @@ module Ci
belongs_to :group
- validates :key, uniqueness: { scope: :group_id }
+ validates :key, uniqueness: {
+ scope: :group_id,
+ message: "(%{value}) has already been taken"
+ }
scope :unprotected, -> { where(protected: false) }
end
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 13c784bea0d..609620a62bb 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -49,7 +49,7 @@ module Ci
ref_protected: 1
}
- cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at
+ cached_attr_reader :version, :revision, :platform, :architecture, :contacted_at, :ip_address
# Searches for runners matching the given query.
#
@@ -157,7 +157,7 @@ module Ci
end
def update_cached_info(values)
- values = values&.slice(:version, :revision, :platform, :architecture) || {}
+ values = values&.slice(:version, :revision, :platform, :architecture, :ip_address) || {}
values[:contacted_at] = Time.now
cache_attributes(values)
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 67d3ec81b6f..7c71291de84 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -6,7 +6,10 @@ module Ci
belongs_to :project
- validates :key, uniqueness: { scope: [:project_id, :environment_scope] }
+ validates :key, uniqueness: {
+ scope: [:project_id, :environment_scope],
+ message: "(%{value}) has already been taken"
+ }
scope :unprotected, -> { where(protected: false) }
end
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index 193bb48e54d..58de3448577 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -15,7 +15,7 @@ module Clusters
end
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, install_helm: true)
+ Gitlab::Kubernetes::Helm::InitCommand.new(name)
end
end
end
diff --git a/app/models/clusters/applications/ingress.rb b/app/models/clusters/applications/ingress.rb
index aa5cf97756f..27fc3b85465 100644
--- a/app/models/clusters/applications/ingress.rb
+++ b/app/models/clusters/applications/ingress.rb
@@ -5,6 +5,8 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
+ include AfterCommitQueue
default_value_for :ingress_type, :nginx
default_value_for :version, :nginx
@@ -13,16 +15,34 @@ module Clusters
nginx: 1
}
+ FETCH_IP_ADDRESS_DELAY = 30.seconds
+
+ state_machine :status do
+ before_transition any => [:installed] do |application|
+ application.run_after_commit do
+ ClusterWaitForIngressIpAddressWorker.perform_in(
+ FETCH_IP_ADDRESS_DELAY, application.name, application.id)
+ end
+ end
+ end
+
def chart
'stable/nginx-ingress'
end
- def chart_values_file
- "#{Rails.root}/vendor/#{name}/values.yaml"
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values
+ )
end
- def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
+ def schedule_status_update
+ return unless installed?
+ return if external_ip
+
+ ClusterWaitForIngressIpAddressWorker.perform_async(name, id)
end
end
end
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index ba63d0f3c64..7b25d8c4089 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -9,6 +9,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
default_value_for :version, VERSION
@@ -32,12 +33,12 @@ module Clusters
80
end
- def chart_values_file
- "#{Rails.root}/vendor/#{name}/values.yaml"
- end
-
def install_command
- Gitlab::Kubernetes::Helm::InstallCommand.new(name, chart: chart, chart_values_file: chart_values_file)
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values
+ )
end
def prometheus_client
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
new file mode 100644
index 00000000000..16efe90fa27
--- /dev/null
+++ b/app/models/clusters/applications/runner.rb
@@ -0,0 +1,69 @@
+module Clusters
+ module Applications
+ class Runner < ActiveRecord::Base
+ VERSION = '0.1.13'.freeze
+
+ self.table_name = 'clusters_applications_runners'
+
+ include ::Clusters::Concerns::ApplicationCore
+ include ::Clusters::Concerns::ApplicationStatus
+ include ::Clusters::Concerns::ApplicationData
+
+ belongs_to :runner, class_name: 'Ci::Runner', foreign_key: :runner_id
+ delegate :project, to: :cluster
+
+ default_value_for :version, VERSION
+
+ def chart
+ "#{name}/gitlab-runner"
+ end
+
+ def repository
+ 'https://charts.gitlab.io'
+ end
+
+ def values
+ content_values.to_yaml
+ end
+
+ def install_command
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ name,
+ chart: chart,
+ values: values,
+ repository: repository
+ )
+ end
+
+ private
+
+ def ensure_runner
+ runner || create_and_assign_runner
+ end
+
+ def create_and_assign_runner
+ transaction do
+ project.runners.create!(name: 'kubernetes-cluster', tag_list: %w(kubernetes cluster)).tap do |runner|
+ update!(runner_id: runner.id)
+ end
+ end
+ end
+
+ def gitlab_url
+ Gitlab::Routing.url_helpers.root_url(only_path: false)
+ end
+
+ def specification
+ {
+ "gitlabUrl" => gitlab_url,
+ "runnerToken" => ensure_runner.token,
+ "runners" => { "privileged" => privileged }
+ }
+ end
+
+ def content_values
+ YAML.load_file(chart_values_file).deep_merge!(specification)
+ end
+ end
+ end
+end
diff --git a/app/models/clusters/cluster.rb b/app/models/clusters/cluster.rb
index 5ecbd4cbceb..49eb069016a 100644
--- a/app/models/clusters/cluster.rb
+++ b/app/models/clusters/cluster.rb
@@ -7,7 +7,8 @@ module Clusters
APPLICATIONS = {
Applications::Helm.application_name => Applications::Helm,
Applications::Ingress.application_name => Applications::Ingress,
- Applications::Prometheus.application_name => Applications::Prometheus
+ Applications::Prometheus.application_name => Applications::Prometheus,
+ Applications::Runner.application_name => Applications::Runner
}.freeze
belongs_to :user
@@ -23,6 +24,7 @@ module Clusters
has_one :application_helm, class_name: 'Clusters::Applications::Helm'
has_one :application_ingress, class_name: 'Clusters::Applications::Ingress'
has_one :application_prometheus, class_name: 'Clusters::Applications::Prometheus'
+ has_one :application_runner, class_name: 'Clusters::Applications::Runner'
accepts_nested_attributes_for :provider_gcp, update_only: true
accepts_nested_attributes_for :platform_kubernetes, update_only: true
@@ -65,7 +67,8 @@ module Clusters
[
application_helm || build_application_helm,
application_ingress || build_application_ingress,
- application_prometheus || build_application_prometheus
+ application_prometheus || build_application_prometheus,
+ application_runner || build_application_runner
]
end
diff --git a/app/models/clusters/concerns/application_core.rb b/app/models/clusters/concerns/application_core.rb
index a98fa85a5ff..623b836c0ed 100644
--- a/app/models/clusters/concerns/application_core.rb
+++ b/app/models/clusters/concerns/application_core.rb
@@ -23,6 +23,11 @@ module Clusters
def name
self.class.application_name
end
+
+ def schedule_status_update
+ # Override if you need extra data synchronized
+ # from K8s after installation
+ end
end
end
end
diff --git a/app/models/clusters/concerns/application_data.rb b/app/models/clusters/concerns/application_data.rb
new file mode 100644
index 00000000000..96ac757e99e
--- /dev/null
+++ b/app/models/clusters/concerns/application_data.rb
@@ -0,0 +1,23 @@
+module Clusters
+ module Concerns
+ module ApplicationData
+ extend ActiveSupport::Concern
+
+ included do
+ def repository
+ nil
+ end
+
+ def values
+ File.read(chart_values_file)
+ end
+
+ private
+
+ def chart_values_file
+ "#{Rails.root}/vendor/#{name}/values.yaml"
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index add5fcf0e79..b9106309142 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -19,6 +19,7 @@ class Commit
attr_accessor :project, :author
attr_accessor :redacted_description_html
attr_accessor :redacted_title_html
+ attr_reader :gpg_commit
DIFF_SAFE_LINES = Gitlab::Git::DiffCollection::DEFAULT_LIMITS[:max_lines]
@@ -110,6 +111,7 @@ class Commit
@raw = raw_commit
@project = project
@statuses = {}
+ @gpg_commit = Gitlab::Gpg::Commit.new(self) if project
end
def id
@@ -452,8 +454,4 @@ class Commit
def merged_merge_request_no_cache(user)
MergeRequestsFinder.new(user, project_id: project.id).find_by(merge_commit_sha: id) if merge_commit?
end
-
- def gpg_commit
- @gpg_commit ||= Gitlab::Gpg::Commit.new(self)
- end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 3469d5d795c..9fb5b7efec6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -141,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end
def group_name
- name.to_s.gsub(%r{\d+[\s:/\\]+\d+\s*}, '').strip
+ name.to_s.gsub(%r{\d+[\.\s:/\\]+\d+\s*}, '').strip
end
def failed_but_allowed?
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
index 62bc6b809f4..d502e7e54c6 100644
--- a/app/models/concerns/access_requestable.rb
+++ b/app/models/concerns/access_requestable.rb
@@ -8,6 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
- Members::RequestAccessService.new(self, user).execute
+ Members::RequestAccessService.new(user).execute(self)
end
end
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index 7049f340c9d..4560bc23193 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -19,6 +19,7 @@ module Issuable
include AfterCommitQueue
include Sortable
include CreatedAtFilterable
+ include UpdatedAtFilterable
# This object is used to gather issuable meta data for displaying
# upvotes, downvotes, notes and closing merge requests count for issues and merge requests
diff --git a/app/models/concerns/updated_at_filterable.rb b/app/models/concerns/updated_at_filterable.rb
new file mode 100644
index 00000000000..edb423b7828
--- /dev/null
+++ b/app/models/concerns/updated_at_filterable.rb
@@ -0,0 +1,12 @@
+module UpdatedAtFilterable
+ extend ActiveSupport::Concern
+
+ included do
+ scope :updated_before, ->(date) { where(scoped_table[:updated_at].lteq(date)) }
+ scope :updated_after, ->(date) { where(scoped_table[:updated_at].gteq(date)) }
+
+ def self.scoped_table
+ arel_table.alias(table_name)
+ end
+ end
+end
diff --git a/app/models/cycle_analytics.rb b/app/models/cycle_analytics.rb
index d2e626c22e8..b34d1382d43 100644
--- a/app/models/cycle_analytics.rb
+++ b/app/models/cycle_analytics.rb
@@ -6,6 +6,12 @@ class CycleAnalytics
@options = options
end
+ def all_medians_per_stage
+ STAGES.each_with_object({}) do |stage_name, medians_per_stage|
+ medians_per_stage[stage_name] = self[stage_name].median
+ end
+ end
+
def summary
@summary ||= ::Gitlab::CycleAnalytics::StageSummary.new(@project,
from: @options[:from],
diff --git a/app/models/event.rb b/app/models/event.rb
index 75538ba196c..be0fc7efa9a 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -158,7 +158,7 @@ class Event < ActiveRecord::Base
def project_name
if project
- project.name_with_namespace
+ project.full_name
else
"(deleted project)"
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 75bf013ecd2..201505c3d3c 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -31,6 +31,8 @@ class Group < Namespace
has_many :uploads, as: :model, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ has_many :badges, class_name: 'GroupBadge'
+
accepts_nested_attributes_for :variables, allow_destroy: true
validate :visibility_level_allowed_by_projects
diff --git a/app/models/identity.rb b/app/models/identity.rb
index 2b433e9b988..1011b9f1109 100644
--- a/app/models/identity.rb
+++ b/app/models/identity.rb
@@ -17,12 +17,12 @@ class Identity < ActiveRecord::Base
end
def ldap?
- Gitlab::OAuth::Provider.ldap_provider?(provider)
+ Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
end
def self.normalize_uid(provider, uid)
- if Gitlab::OAuth::Provider.ldap_provider?(provider)
- Gitlab::LDAP::Person.normalize_dn(uid)
+ if Gitlab::Auth::OAuth::Provider.ldap_provider?(provider)
+ Gitlab::Auth::LDAP::Person.normalize_dn(uid)
else
uid.to_s
end
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index fc586fa216e..b444812a4cf 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -15,4 +15,8 @@ class LfsObject < ActiveRecord::Base
.where(lfs_objects_projects: { id: nil })
.destroy_all
end
+
+ def self.calculate_oid(path)
+ Digest::SHA256.file(path).hexdigest
+ end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index 2d17795e62d..408e8b2d704 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -128,7 +128,7 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
- def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil)
+ def add_user(source, user, access_level, existing_members: nil, current_user: nil, expires_at: nil, ldap: false)
# `user` can be either a User object, User ID or an email to be invited
member = retrieve_member(source, user, existing_members)
access_level = retrieve_access_level(access_level)
@@ -143,11 +143,13 @@ class Member < ActiveRecord::Base
if member.request?
::Members::ApproveAccessRequestService.new(
- source,
current_user,
- id: member.id,
access_level: access_level
- ).execute
+ ).execute(
+ member,
+ skip_authorization: ldap,
+ skip_log_audit_event: ldap
+ )
else
member.save
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index c1c27ccf3e5..06aa67c600f 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -197,10 +197,6 @@ class MergeRequestDiff < ActiveRecord::Base
CompareService.new(project, head_commit_sha).execute(project, sha, straight: true)
end
- def commits_count
- super || merge_request_diff_commits.size
- end
-
private
def create_merge_request_diff_files(diffs)
diff --git a/app/models/network/commit.rb b/app/models/network/commit.rb
index 9357e55b419..22d48c9e661 100644
--- a/app/models/network/commit.rb
+++ b/app/models/network/commit.rb
@@ -24,12 +24,7 @@ module Network
end
def parents(map)
- @commit.parents.map do |p|
- if map.include?(p.id)
- map[p.id]
- end
- end
- .compact
+ map.values_at(*@commit.parent_ids).compact
end
end
end
diff --git a/app/models/note.rb b/app/models/note.rb
index cac60845a49..d7a67ec277c 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -133,6 +133,7 @@ class Note < ActiveRecord::Base
def find_discussion(discussion_id)
notes = where(discussion_id: discussion_id).fresh.to_a
+
return if notes.empty?
Discussion.build(notes)
diff --git a/app/models/project.rb b/app/models/project.rb
index ba278a49688..a11b1e4f554 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -221,6 +221,8 @@ class Project < ActiveRecord::Base
has_one :auto_devops, class_name: 'ProjectAutoDevops'
has_many :custom_attributes, class_name: 'ProjectCustomAttribute'
+ has_many :project_badges, class_name: 'ProjectBadge'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :import_data
@@ -317,42 +319,13 @@ class Project < ActiveRecord::Base
# Returns a collection of projects that is either public or visible to the
# logged in user.
- #
- # A caller may pass in a block to modify individual parts of
- # the query, e.g. to apply .with_feature_available_for_user on top of it.
- # This is useful for performance as we can stick those additional filters
- # at the bottom of e.g. the UNION.
- #
- # Optionally, turning `use_where_in` off leads to returning a
- # relation using #from instead of #where. This can perform much better
- # but leads to trouble when used in conjunction with AR's #merge method.
- def self.public_or_visible_to_user(user = nil, use_where_in: true, &block)
- # If we don't get a block passed, use identity to avoid if/else repetitions
- block = ->(part) { part } unless block_given?
-
- return block.call(public_to_user) unless user
-
- # If the user is allowed to see all projects,
- # we can shortcut and just return.
- return block.call(all) if user.full_private_access?
-
- authorized = user
- .project_authorizations
- .select(1)
- .where('project_authorizations.project_id = projects.id')
- authorized_projects = block.call(where('EXISTS (?)', authorized))
-
- levels = Gitlab::VisibilityLevel.levels_for_user(user)
- visible_projects = block.call(where(visibility_level: levels))
-
- # We use a UNION here instead of OR clauses since this results in better
- # performance.
- union = Gitlab::SQL::Union.new([authorized_projects.select('projects.id'), visible_projects.select('projects.id')])
-
- if use_where_in
- where("projects.id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ def self.public_or_visible_to_user(user = nil)
+ if user
+ where('EXISTS (?) OR projects.visibility_level IN (?)',
+ user.authorizations_for_projects,
+ Gitlab::VisibilityLevel.levels_for_user(user))
else
- from("(#{union.to_sql}) AS #{table_name}")
+ public_to_user
end
end
@@ -371,14 +344,11 @@ class Project < ActiveRecord::Base
elsif user
column = ProjectFeature.quoted_access_level_column(feature)
- authorized = user.project_authorizations.select(1)
- .where('project_authorizations.project_id = projects.id')
-
with_project_feature
.where("#{column} IN (?) OR (#{column} = ? AND EXISTS (?))",
visible,
ProjectFeature::PRIVATE,
- authorized)
+ user.authorizations_for_projects)
else
with_feature_access_level(feature, visible)
end
@@ -1798,6 +1768,17 @@ class Project < ActiveRecord::Base
.set(import_jid, StuckImportJobsWorker::IMPORT_JOBS_EXPIRATION)
end
+ def badges
+ return project_badges unless group
+
+ group_badges_rel = GroupBadge.where(group: group.self_and_ancestors)
+
+ union = Gitlab::SQL::Union.new([project_badges.select(:id),
+ group_badges_rel.select(:id)])
+
+ Badge.where("id IN (#{union.to_sql})") # rubocop:disable GitlabSecurity/SqlInjection
+ end
+
private
def storage
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 109258d1eb7..4f289e6e215 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -68,7 +68,7 @@ http://app.asana.com/-/account_api'
end
user = data[:user_name]
- project_name = project.name_with_namespace
+ project_name = project.full_name
data[:commits].each do |commit|
push_msg = "#{user} pushed to branch #{branch} of #{project_name} ( #{commit[:url]} ):"
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index c3f5b310619..8d7a4fceb08 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -86,7 +86,7 @@ class CampfireService < Service
after = push[:after]
message = ""
- message << "[#{project.name_with_namespace}] "
+ message << "[#{project.full_name}] "
message << "#{push[:user_name]} "
if Gitlab::Git.blank_ref?(before)
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 818cfb01b14..dab0ea1a681 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -99,7 +99,7 @@ class ChatNotificationService < Service
def get_message(object_kind, data)
case object_kind
when "push", "tag_push"
- ChatMessage::PushMessage.new(data)
+ ChatMessage::PushMessage.new(data) if notify_for_ref?(data)
when "issue"
ChatMessage::IssueMessage.new(data) unless update?(data)
when "merge_request"
@@ -129,7 +129,7 @@ class ChatNotificationService < Service
end
def project_name
- project.name_with_namespace.gsub(/\s/, '')
+ project.full_name.gsub(/\s/, '')
end
def project_url
@@ -145,10 +145,16 @@ class ChatNotificationService < Service
end
def notify_for_ref?(data)
- return true if data[:object_attributes][:tag]
+ return true if data.dig(:object_attributes, :tag)
return true unless notify_only_default_branch?
- data[:object_attributes][:ref] == project.default_branch
+ ref = if data[:ref]
+ Gitlab::Git.ref_name(data[:ref])
+ else
+ data.dig(:object_attributes, :ref)
+ end
+
+ ref == project.default_branch
end
def notify_for_pipeline?(data)
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index bfe7ac29c18..f31c3f02af2 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -120,7 +120,7 @@ class HipchatService < Service
else
message << "pushed to #{ref_type} <a href=\""\
"#{project.web_url}/commits/#{CGI.escape(ref)}\">#{ref}</a> "
- message << "of <a href=\"#{project.web_url}\">#{project.name_with_namespace.gsub!(/\s/, '')}</a> "
+ message << "of <a href=\"#{project.web_url}\">#{project.full_name.gsub!(/\s/, '')}</a> "
message << "(<a href=\"#{project.web_url}/compare/#{before}...#{after}\">Compare changes</a>)"
push[:commits].take(MAX_COMMITS).each do |commit|
@@ -274,7 +274,7 @@ class HipchatService < Service
end
def project_name
- project.name_with_namespace.gsub(/\s/, '')
+ project.full_name.gsub(/\s/, '')
end
def project_url
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 436a870b0c4..e5035c81df0 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -1,5 +1,7 @@
class JiraService < IssueTrackerService
include Gitlab::Routing
+ include ApplicationHelper
+ include ActionView::Helpers::AssetUrlHelper
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
@@ -268,7 +270,9 @@ class JiraService < IssueTrackerService
url: url,
title: title,
status: status,
- icon: { title: 'GitLab', url16x16: 'https://gitlab.com/favicon.ico' }
+ icon: {
+ title: 'GitLab', url16x16: asset_url('favicon.ico', host: gitlab_config.url)
+ }
}
}
end
diff --git a/app/models/project_services/mattermost_slash_commands_service.rb b/app/models/project_services/mattermost_slash_commands_service.rb
index 4d2037286a2..227d430083d 100644
--- a/app/models/project_services/mattermost_slash_commands_service.rb
+++ b/app/models/project_services/mattermost_slash_commands_service.rb
@@ -37,7 +37,7 @@ class MattermostSlashCommandsService < SlashCommandsService
private
def command(params)
- pretty_project_name = project.name_with_namespace
+ pretty_project_name = project.full_name
params.merge(
auto_complete: true,
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index aa7bd4c3c84..e3a1ca2d45f 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -88,10 +88,10 @@ class PushoverService < Service
user: user_key,
device: device,
priority: priority,
- title: "#{project.name_with_namespace}",
+ title: "#{project.full_name}",
message: message,
url: data[:project][:web_url],
- url_title: "See project #{project.name_with_namespace}"
+ url_title: "See project #{project.full_name}"
}
# Sound parameter MUST NOT be sent to API if not selected
diff --git a/app/models/project_services/slash_commands_service.rb b/app/models/project_services/slash_commands_service.rb
index eb4da68bb7e..37ea45109ae 100644
--- a/app/models/project_services/slash_commands_service.rb
+++ b/app/models/project_services/slash_commands_service.rb
@@ -30,10 +30,10 @@ class SlashCommandsService < Service
def trigger(params)
return unless valid_token?(params[:token])
- user = find_chat_user(params)
+ chat_user = find_chat_user(params)
- if user
- Gitlab::SlashCommands::Command.new(project, user, params).execute
+ if chat_user&.user
+ Gitlab::SlashCommands::Command.new(project, chat_user, params).execute
else
url = authorize_chat_name_url(params)
Gitlab::SlashCommands::Presenters::Access.new(url).authorize
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 299a3f32a85..1a14afb951a 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -139,7 +139,7 @@ class Repository
end
end
- def commits(ref, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil)
+ def commits(ref = nil, path: nil, limit: nil, offset: nil, skip_merges: false, after: nil, before: nil, all: nil)
options = {
repo: raw_repository,
ref: ref,
@@ -149,7 +149,8 @@ class Repository
after: after,
before: before,
follow: Array(path).length == 1,
- skip_merges: skip_merges
+ skip_merges: skip_merges,
+ all: all
}
commits = Gitlab::Git::Commit.where(options)
@@ -252,7 +253,7 @@ class Repository
# branches or tags, but we want to keep some of these commits around, for
# example if they have comments or CI builds.
def keep_around(sha)
- return unless sha && commit_by(oid: sha)
+ return unless sha.present? && commit_by(oid: sha)
return if kept_around?(sha)
@@ -589,15 +590,7 @@ class Repository
def license_key
return unless exists?
- # The licensee gem creates a Rugged object from the path:
- # https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
- begin
- Licensee.license(path).try(:key)
- # Normally we would rescue Rugged::Error, but that is banned by lint-rugged
- # and we need to migrate this endpoint to Gitaly:
- # https://gitlab.com/gitlab-org/gitaly/issues/1026
- rescue
- end
+ raw_repository.license_short_name
end
cache_method :license_key
diff --git a/app/models/todo.rb b/app/models/todo.rb
index bb5965e20eb..8afacd188e0 100644
--- a/app/models/todo.rb
+++ b/app/models/todo.rb
@@ -32,8 +32,6 @@ class Todo < ActiveRecord::Base
validates :target_id, presence: true, unless: :for_commit?
validates :commit_id, presence: true, if: :for_commit?
- default_scope { reorder(id: :desc) }
-
scope :pending, -> { with_state(:pending) }
scope :done, -> { with_state(:done) }
@@ -53,10 +51,14 @@ class Todo < ActiveRecord::Base
# milestones, but still show something if the user has a URL with that
# selected.
def sort(method)
- case method.to_s
- when 'priority', 'label_priority' then order_by_labels_priority
- else order_by(method)
- end
+ sorted =
+ case method.to_s
+ when 'priority', 'label_priority' then order_by_labels_priority
+ else order_by(method)
+ end
+
+ # Break ties with the ID column for pagination
+ sorted.order(id: :desc)
end
# Order by priority depending on which issue/merge request the Todo belongs to
diff --git a/app/models/tree.rb b/app/models/tree.rb
index c89b8eca9be..4c1856b67a8 100644
--- a/app/models/tree.rb
+++ b/app/models/tree.rb
@@ -9,10 +9,9 @@ class Tree
@repository = repository
@sha = sha
@path = path
- @recursive = recursive
git_repo = @repository.raw_repository
- @entries = get_entries(git_repo, @sha, @path, recursive: @recursive)
+ @entries = Gitlab::Git::Tree.where(git_repo, @sha, @path, recursive)
end
def readme
@@ -58,21 +57,4 @@ class Tree
def sorted_entries
trees + blobs + submodules
end
-
- private
-
- def get_entries(git_repo, sha, path, recursive: false)
- current_path_entries = Gitlab::Git::Tree.where(git_repo, sha, path)
- ordered_entries = []
-
- current_path_entries.each do |entry|
- ordered_entries << entry
-
- if recursive && entry.dir?
- ordered_entries.concat(get_entries(git_repo, sha, entry.path, recursive: true))
- end
- end
-
- ordered_entries
- end
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 8610ca27b7f..9c60adf0c90 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -431,7 +431,7 @@ class User < ActiveRecord::Base
end
def self.non_internal
- where(Hash[internal_attributes.zip([[false, nil]] * internal_attributes.size)])
+ where(internal_attributes.map { |attr| "#{attr} IS NOT TRUE" }.join(" AND "))
end
#
@@ -601,6 +601,15 @@ class User < ActiveRecord::Base
authorized_projects(min_access_level).exists?({ id: project.id })
end
+ # Typically used in conjunction with projects table to get projects
+ # a user has been given access to.
+ #
+ # Example use:
+ # `Project.where('EXISTS(?)', user.authorizations_for_projects)`
+ def authorizations_for_projects
+ project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ end
+
# Returns the projects this user has reporter (or greater) access to, limited
# to at most the given projects.
#
@@ -728,7 +737,7 @@ class User < ActiveRecord::Base
def ldap_user?
if identities.loaded?
- identities.find { |identity| Gitlab::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
+ identities.find { |identity| Gitlab::Auth::OAuth::Provider.ldap_provider?(identity.provider) && !identity.extern_uid.nil? }
else
identities.exists?(["provider LIKE ? AND extern_uid IS NOT NULL", "ldap%"])
end
diff --git a/app/models/user_synced_attributes_metadata.rb b/app/models/user_synced_attributes_metadata.rb
index 548b99b69d9..688432a9d67 100644
--- a/app/models/user_synced_attributes_metadata.rb
+++ b/app/models/user_synced_attributes_metadata.rb
@@ -26,6 +26,6 @@ class UserSyncedAttributesMetadata < ActiveRecord::Base
private
def sync_profile_from_provider?
- Gitlab::OAuth::Provider.sync_profile_from_provider?(provider)
+ Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(provider)
end
end
diff --git a/app/serializers/analytics_stage_entity.rb b/app/serializers/analytics_stage_entity.rb
index 564612202b5..3e355a13e06 100644
--- a/app/serializers/analytics_stage_entity.rb
+++ b/app/serializers/analytics_stage_entity.rb
@@ -7,6 +7,7 @@ class AnalyticsStageEntity < Grape::Entity
expose :description
expose :median, as: :value do |stage|
- stage.median && !stage.median.zero? ? distance_of_time_in_words(stage.median) : nil
+ # median returns a BatchLoader instance which we first have to unwrap by using to_i
+ !stage.median.to_i.zero? ? distance_of_time_in_words(stage.median) : nil
end
end
diff --git a/app/serializers/cluster_application_entity.rb b/app/serializers/cluster_application_entity.rb
index 3f9a275ad08..b22a0b666ef 100644
--- a/app/serializers/cluster_application_entity.rb
+++ b/app/serializers/cluster_application_entity.rb
@@ -2,4 +2,5 @@ class ClusterApplicationEntity < Grape::Entity
expose :name
expose :status_name, as: :status
expose :status_reason
+ expose :external_ip, if: -> (e, _) { e.respond_to?(:external_ip) }
end
diff --git a/app/serializers/diff_file_entity.rb b/app/serializers/diff_file_entity.rb
new file mode 100644
index 00000000000..6e68d275047
--- /dev/null
+++ b/app/serializers/diff_file_entity.rb
@@ -0,0 +1,41 @@
+class DiffFileEntity < Grape::Entity
+ include DiffHelper
+ include SubmoduleHelper
+ include BlobHelper
+ include IconsHelper
+ include ActionView::Helpers::TagHelper
+
+ expose :submodule?, as: :submodule
+
+ expose :submodule_link do |diff_file|
+ submodule_links(diff_file.blob, diff_file.content_sha, diff_file.repository).first
+ end
+
+ expose :blob_path do |diff_file|
+ diff_file.blob.path
+ end
+
+ expose :blob_icon do |diff_file|
+ blob_icon(diff_file.b_mode, diff_file.file_path)
+ end
+
+ expose :file_path
+ expose :deleted_file?, as: :deleted_file
+ expose :renamed_file?, as: :renamed_file
+ expose :old_path
+ expose :new_path
+ expose :mode_changed?, as: :mode_changed
+ expose :a_mode
+ expose :b_mode
+ expose :text?, as: :text
+
+ expose :old_path_html do |diff_file|
+ old_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ old_path
+ end
+
+ expose :new_path_html do |diff_file|
+ _, new_path = mark_inline_diffs(diff_file.old_path, diff_file.new_path)
+ new_path
+ end
+end
diff --git a/app/serializers/discussion_entity.rb b/app/serializers/discussion_entity.rb
index 0a92e3f8167..bbbcf6a97c1 100644
--- a/app/serializers/discussion_entity.rb
+++ b/app/serializers/discussion_entity.rb
@@ -7,4 +7,42 @@ class DiscussionEntity < Grape::Entity
expose :notes, using: NoteEntity
expose :individual_note?, as: :individual_note
+ expose :resolvable?, as: :resolvable
+ expose :resolved?, as: :resolved
+ expose :resolve_path, if: -> (d, _) { d.resolvable? } do |discussion|
+ resolve_project_merge_request_discussion_path(discussion.project, discussion.noteable, discussion.id)
+ end
+ expose :resolve_with_issue_path do |discussion|
+ new_project_issue_path(discussion.project, merge_request_to_resolve_discussions_of: discussion.noteable.iid, discussion_to_resolve: discussion.id)
+ end
+
+ expose :diff_file, using: DiffFileEntity, if: -> (d, _) { defined? d.diff_file }
+
+ expose :diff_discussion?, as: :diff_discussion
+
+ expose :truncated_diff_lines, if: -> (d, _) { (defined? d.diff_file) && d.diff_file.text? } do |discussion|
+ options[:context].render_to_string(
+ partial: "projects/diffs/line",
+ collection: discussion.truncated_diff_lines,
+ as: :line,
+ locals: { diff_file: discussion.diff_file,
+ discussion_expanded: true,
+ plain: true },
+ layout: false,
+ formats: [:html]
+ )
+ end
+
+ expose :image_diff_html, if: -> (d, _) { defined? d.diff_file } do |discussion|
+ diff_file = discussion.diff_file
+ partial = diff_file.new_file? || diff_file.deleted_file? ? 'single_image_diff' : 'replaced_image_diff'
+ options[:context].render_to_string(
+ partial: "projects/diffs/#{partial}",
+ locals: { diff_file: diff_file,
+ position: discussion.position.to_json,
+ click_to_comment: false },
+ layout: false,
+ formats: [:html]
+ )
+ end
end
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index fbfe480503b..4e8ef320af2 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -115,6 +115,14 @@ class MergeRequestWidgetEntity < IssuableEntity
expose :can_cherry_pick_on_current_merge_request do |merge_request|
presenter(merge_request).can_cherry_pick_on_current_merge_request?
end
+
+ expose :can_create_note do |issue|
+ can?(request.current_user, :create_note, issue.project)
+ end
+
+ expose :can_update do |issue|
+ can?(request.current_user, :update_issue, issue)
+ end
end
# Paths
@@ -189,6 +197,10 @@ class MergeRequestWidgetEntity < IssuableEntity
end
end
+ expose :create_note_path do |merge_request|
+ project_notes_path(merge_request.project, target_type: 'merge_request', target_id: merge_request.id)
+ end
+
expose :commit_change_content_path do |merge_request|
commit_change_content_project_merge_request_path(merge_request.project, merge_request)
end
diff --git a/app/serializers/note_entity.rb b/app/serializers/note_entity.rb
index 7d50e0ff10d..4ccf0bca476 100644
--- a/app/serializers/note_entity.rb
+++ b/app/serializers/note_entity.rb
@@ -23,6 +23,10 @@ class NoteEntity < API::Entities::Note
end
end
+ expose :resolved?, as: :resolved
+ expose :resolvable?, as: :resolvable
+ expose :resolved_by, using: NoteUserEntity
+
expose :system_note_icon_name, if: -> (note, _) { note.system? } do |note|
SystemNoteHelper.system_note_icon_name(note)
end
@@ -53,6 +57,14 @@ class NoteEntity < API::Entities::Note
end
end
+ expose :resolve_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ resolve_project_merge_request_discussion_path(note.project, note.noteable, note.discussion_id)
+ end
+
+ expose :resolve_with_issue_path, if: -> (note, _) { note.part_of_discussion? && note.resolvable? } do |note|
+ new_project_issue_path(note.project, merge_request_to_resolve_discussions_of: note.noteable.iid, discussion_to_resolve: note.discussion_id)
+ end
+
expose :attachment, using: NoteAttachmentEntity, if: -> (note, _) { note.attachment? }
expose :delete_attachment_path, if: -> (note, _) { note.attachment? } do |note|
delete_attachment_project_note_path(note.project, note)
diff --git a/app/services/badges/base_service.rb b/app/services/badges/base_service.rb
new file mode 100644
index 00000000000..4f87426bd38
--- /dev/null
+++ b/app/services/badges/base_service.rb
@@ -0,0 +1,11 @@
+module Badges
+ class BaseService
+ protected
+
+ attr_accessor :params
+
+ def initialize(params = {})
+ @params = params.dup
+ end
+ end
+end
diff --git a/app/services/badges/build_service.rb b/app/services/badges/build_service.rb
new file mode 100644
index 00000000000..6267e571838
--- /dev/null
+++ b/app/services/badges/build_service.rb
@@ -0,0 +1,12 @@
+module Badges
+ class BuildService < Badges::BaseService
+ # returns the created badge
+ def execute(source)
+ if source.is_a?(Group)
+ GroupBadge.new(params.merge(group: source))
+ else
+ ProjectBadge.new(params.merge(project: source))
+ end
+ end
+ end
+end
diff --git a/app/services/badges/create_service.rb b/app/services/badges/create_service.rb
new file mode 100644
index 00000000000..aafb87f7dcd
--- /dev/null
+++ b/app/services/badges/create_service.rb
@@ -0,0 +1,10 @@
+module Badges
+ class CreateService < Badges::BaseService
+ # returns the created badge
+ def execute(source)
+ badge = Badges::BuildService.new(params).execute(source)
+
+ badge.tap { |b| b.save }
+ end
+ end
+end
diff --git a/app/services/badges/update_service.rb b/app/services/badges/update_service.rb
new file mode 100644
index 00000000000..7ca84b5df31
--- /dev/null
+++ b/app/services/badges/update_service.rb
@@ -0,0 +1,12 @@
+module Badges
+ class UpdateService < Badges::BaseService
+ # returns the updated badge
+ def execute(badge)
+ if params.present?
+ badge.update_attributes(params)
+ end
+
+ badge
+ end
+ end
+end
diff --git a/app/services/chat_names/find_user_service.rb b/app/services/chat_names/find_user_service.rb
index 4f5c5567b42..d458b814183 100644
--- a/app/services/chat_names/find_user_service.rb
+++ b/app/services/chat_names/find_user_service.rb
@@ -9,8 +9,8 @@ module ChatNames
chat_name = find_chat_name
return unless chat_name
- chat_name.touch(:last_used_at)
- chat_name.user
+ chat_name.update_last_used_at
+ chat_name
end
private
diff --git a/app/services/ci/create_trace_artifact_service.rb b/app/services/ci/create_trace_artifact_service.rb
index 280a2c3afa4..ffde824972c 100644
--- a/app/services/ci/create_trace_artifact_service.rb
+++ b/app/services/ci/create_trace_artifact_service.rb
@@ -4,13 +4,33 @@ module Ci
return if job.job_artifacts_trace
job.trace.read do |stream|
- if stream.file?
- job.create_job_artifacts_trace!(
- project: job.project,
- file_type: :trace,
- file: stream)
+ break unless stream.file?
+
+ clone_file!(stream.path, JobArtifactUploader.workhorse_upload_path) do |clone_path|
+ create_job_trace!(job, clone_path)
+ FileUtils.rm(stream.path)
end
end
end
+
+ private
+
+ def create_job_trace!(job, path)
+ File.open(path) do |stream|
+ job.create_job_artifacts_trace!(
+ project: job.project,
+ file_type: :trace,
+ file: stream)
+ end
+ end
+
+ def clone_file!(src_path, temp_dir)
+ FileUtils.mkdir_p(temp_dir)
+ Dir.mktmpdir('tmp-trace', temp_dir) do |dir_path|
+ temp_path = File.join(dir_path, "job.log")
+ FileUtils.copy(src_path, temp_path)
+ yield(temp_path)
+ end
+ end
end
end
diff --git a/app/services/clusters/applications/check_ingress_ip_address_service.rb b/app/services/clusters/applications/check_ingress_ip_address_service.rb
new file mode 100644
index 00000000000..e572b1e5d99
--- /dev/null
+++ b/app/services/clusters/applications/check_ingress_ip_address_service.rb
@@ -0,0 +1,36 @@
+module Clusters
+ module Applications
+ class CheckIngressIpAddressService < BaseHelmService
+ include Gitlab::Utils::StrongMemoize
+
+ Error = Class.new(StandardError)
+
+ LEASE_TIMEOUT = 15.seconds.to_i
+
+ def execute
+ return if app.external_ip
+ return unless try_obtain_lease
+
+ app.update!(external_ip: ingress_ip) if ingress_ip
+ end
+
+ private
+
+ def try_obtain_lease
+ Gitlab::ExclusiveLease
+ .new("check_ingress_ip_address_service:#{app.id}", timeout: LEASE_TIMEOUT)
+ .try_obtain
+ end
+
+ def ingress_ip
+ service.status.loadBalancer.ingress&.first&.ip
+ end
+
+ def service
+ strong_memoize(:ingress_service) do
+ kubeclient.get_service('ingress-nginx-ingress-controller', Gitlab::Kubernetes::Helm::NAMESPACE)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 66a9b1f82e0..02fb48108fb 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -77,8 +77,12 @@ class IssuableBaseService < BaseService
return unless labels
params[:label_ids] = labels.split(",").map do |label_name|
- service = Labels::FindOrCreateService.new(current_user, project, title: label_name.strip)
- label = service.execute
+ label = Labels::FindOrCreateService.new(
+ current_user,
+ parent,
+ title: label_name.strip,
+ available_labels: available_labels
+ ).execute
label.try(:id)
end.compact
@@ -102,7 +106,11 @@ class IssuableBaseService < BaseService
end
def available_labels
- LabelsFinder.new(current_user, project_id: @project.id).execute
+ @available_labels ||= LabelsFinder.new(current_user, project_id: @project.id).execute
+ end
+
+ def handle_quick_actions_on_create(issuable)
+ merge_quick_actions_into_params!(issuable)
end
def merge_quick_actions_into_params!(issuable)
@@ -127,7 +135,7 @@ class IssuableBaseService < BaseService
end
def create(issuable)
- merge_quick_actions_into_params!(issuable)
+ handle_quick_actions_on_create(issuable)
filter_params(issuable)
params.delete(:state_event)
@@ -303,4 +311,8 @@ class IssuableBaseService < BaseService
def update_project_counter_caches?(issuable)
issuable.state_changed?
end
+
+ def parent
+ project
+ end
end
diff --git a/app/services/labels/find_or_create_service.rb b/app/services/labels/find_or_create_service.rb
index 940c8b333d3..079f611b3f3 100644
--- a/app/services/labels/find_or_create_service.rb
+++ b/app/services/labels/find_or_create_service.rb
@@ -1,8 +1,9 @@
module Labels
class FindOrCreateService
- def initialize(current_user, project, params = {})
+ def initialize(current_user, parent, params = {})
@current_user = current_user
- @project = project
+ @parent = parent
+ @available_labels = params.delete(:available_labels)
@params = params.dup.with_indifferent_access
end
@@ -13,12 +14,13 @@ module Labels
private
- attr_reader :current_user, :project, :params, :skip_authorization
+ attr_reader :current_user, :parent, :params, :skip_authorization
def available_labels
@available_labels ||= LabelsFinder.new(
current_user,
- project_id: project.id
+ "#{parent_type}_id".to_sym => parent.id,
+ only_group_labels: parent_is_group?
).execute(skip_authorization: skip_authorization)
end
@@ -27,8 +29,8 @@ module Labels
def find_or_create_label
new_label = available_labels.find_by(title: title)
- if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, project))
- new_label = Labels::CreateService.new(params).execute(project: project)
+ if new_label.nil? && (skip_authorization || Ability.allowed?(current_user, :admin_label, parent))
+ new_label = Labels::CreateService.new(params).execute(parent_type.to_sym => parent)
end
new_label
@@ -37,5 +39,13 @@ module Labels
def title
params[:title] || params[:name]
end
+
+ def parent_type
+ parent.model_name.param_key
+ end
+
+ def parent_is_group?
+ parent_type == "group"
+ end
end
end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
index 2a2bb0cae5b..6be08b590bc 100644
--- a/app/services/members/approve_access_request_service.rb
+++ b/app/services/members/approve_access_request_service.rb
@@ -1,51 +1,20 @@
module Members
- class ApproveAccessRequestService < BaseService
- include MembersHelper
-
- attr_accessor :source
-
- # source - The source object that respond to `#requesters` (i.g. project or group)
- # current_user - The user that performs the access request approval
- # params - A hash of parameters
- # :user_id - User ID used to retrieve the access requester
- # :id - Member ID used to retrieve the access requester
- # :access_level - Optional access level set when the request is accepted
- def initialize(source, current_user, params = {})
- @source = source
- @current_user = current_user
- @params = params.slice(:user_id, :id, :access_level)
- end
-
- # opts - A hash of options
- # :force - Bypass permission check: current_user can be nil in that case
- def execute(opts = {})
- condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
- access_requester = source.requesters.find_by!(condition)
-
- raise Gitlab::Access::AccessDeniedError unless can_update_access_requester?(access_requester, opts)
+ class ApproveAccessRequestService < Members::BaseService
+ def execute(access_requester, skip_authorization: false, skip_log_audit_event: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_update_access_requester?(access_requester)
access_requester.access_level = params[:access_level] if params[:access_level]
access_requester.accept_request
+ after_execute(member: access_requester, skip_log_audit_event: skip_log_audit_event)
+
access_requester
end
private
- def can_update_access_requester?(access_requester, opts = {})
- access_requester && (
- opts[:force] ||
- can?(current_user, update_member_permission(access_requester), access_requester)
- )
- end
-
- def update_member_permission(member)
- case member
- when GroupMember
- :update_group_member
- when ProjectMember
- :update_project_member
- end
+ def can_update_access_requester?(access_requester)
+ can?(current_user, update_member_permission(access_requester), access_requester)
end
end
end
diff --git a/app/services/members/authorized_destroy_service.rb b/app/services/members/authorized_destroy_service.rb
deleted file mode 100644
index 2e89f00dad8..00000000000
--- a/app/services/members/authorized_destroy_service.rb
+++ /dev/null
@@ -1,61 +0,0 @@
-module Members
- class AuthorizedDestroyService < BaseService
- attr_accessor :member, :user
-
- def initialize(member, user = nil)
- @member, @user = member, user
- end
-
- def execute
- return false if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
-
- Member.transaction do
- unassign_issues_and_merge_requests(member) unless member.invite?
- member.notification_setting&.destroy
-
- member.destroy
- end
-
- if member.request? && member.user != user
- notification_service.decline_access_request(member)
- end
-
- member
- end
-
- private
-
- def unassign_issues_and_merge_requests(member)
- if member.is_a?(GroupMember)
- issues = Issue.unscoped.select(1)
- .joins(:project)
- .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
-
- # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
- IssueAssignee.unscoped
- .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
- .delete_all
-
- MergeRequestsFinder.new(user, group_id: member.source_id, assignee_id: member.user_id)
- .execute
- .update_all(assignee_id: nil)
- else
- project = member.source
-
- # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
- issues = Issue.unscoped.select(1)
- .where('issues.id = issue_assignees.issue_id')
- .where(project_id: project.id)
-
- # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
- IssueAssignee.unscoped
- .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
- .delete_all
-
- project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
- end
-
- member.user.invalidate_cache_counts
- end
- end
-end
diff --git a/app/services/members/base_service.rb b/app/services/members/base_service.rb
new file mode 100644
index 00000000000..74556fb20cf
--- /dev/null
+++ b/app/services/members/base_service.rb
@@ -0,0 +1,49 @@
+module Members
+ class BaseService < ::BaseService
+ # current_user - The user that performs the action
+ # params - A hash of parameters
+ def initialize(current_user = nil, params = {})
+ @current_user = current_user
+ @params = params
+ end
+
+ def after_execute(args)
+ # overriden in EE::Members modules
+ end
+
+ private
+
+ def update_member_permission(member)
+ case member
+ when GroupMember
+ :update_group_member
+ when ProjectMember
+ :update_project_member
+ else
+ raise "Unknown member type: #{member}!"
+ end
+ end
+
+ def override_member_permission(member)
+ case member
+ when GroupMember
+ :override_group_member
+ when ProjectMember
+ :override_project_member
+ else
+ raise "Unknown member type: #{member}!"
+ end
+ end
+
+ def action_member_permission(action, member)
+ case action
+ when :update
+ update_member_permission(member)
+ when :override
+ override_member_permission(member)
+ else
+ raise "Unknown action '#{action}' on #{member}!"
+ end
+ end
+ end
+end
diff --git a/app/services/members/create_service.rb b/app/services/members/create_service.rb
index 26906ae7167..bc6a9405aac 100644
--- a/app/services/members/create_service.rb
+++ b/app/services/members/create_service.rb
@@ -1,15 +1,8 @@
module Members
- class CreateService < BaseService
+ class CreateService < Members::BaseService
DEFAULT_LIMIT = 100
- def initialize(source, current_user, params = {})
- @source = source
- @current_user = current_user
- @params = params
- @error = nil
- end
-
- def execute
+ def execute(source)
return error('No users specified.') if params[:user_ids].blank?
user_ids = params[:user_ids].split(',').uniq
@@ -17,13 +10,15 @@ module Members
return error("Too many users specified (limit is #{user_limit})") if
user_limit && user_ids.size > user_limit
- @source.add_users(
+ members = source.add_users(
user_ids,
params[:access_level],
expires_at: params[:expires_at],
current_user: current_user
)
+ members.each { |member| after_execute(member: member) }
+
success
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 05b93ac8fdb..b141bfd5fbc 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -1,42 +1,30 @@
module Members
- class DestroyService < BaseService
- include MembersHelper
+ class DestroyService < Members::BaseService
+ def execute(member, skip_authorization: false)
+ raise Gitlab::Access::AccessDeniedError unless skip_authorization || can_destroy_member?(member)
- attr_accessor :source
+ return member if member.is_a?(GroupMember) && member.source.last_owner?(member.user)
- ALLOWED_SCOPES = %i[members requesters all].freeze
+ Member.transaction do
+ unassign_issues_and_merge_requests(member) unless member.invite?
+ member.notification_setting&.destroy
- def initialize(source, current_user, params = {})
- @source = source
- @current_user = current_user
- @params = params
- end
-
- def execute(scope = :members)
- raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
+ member.destroy
+ end
- member = find_member!(scope)
+ if member.request? && member.user != current_user
+ notification_service.decline_access_request(member)
+ end
- raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
+ after_execute(member: member)
- AuthorizedDestroyService.new(member, current_user).execute
+ member
end
private
- def find_member!(scope)
- condition = params[:user_id] ? { user_id: params[:user_id] } : { id: params[:id] }
- case scope
- when :all
- source.members.find_by(condition) ||
- source.requesters.find_by!(condition)
- else
- source.public_send(scope).find_by!(condition) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
-
def can_destroy_member?(member)
- member && can?(current_user, destroy_member_permission(member), member)
+ can?(current_user, destroy_member_permission(member), member)
end
def destroy_member_permission(member)
@@ -45,7 +33,42 @@ module Members
:destroy_group_member
when ProjectMember
:destroy_project_member
+ else
+ raise "Unknown member type: #{member}!"
end
end
+
+ def unassign_issues_and_merge_requests(member)
+ if member.is_a?(GroupMember)
+ issues = Issue.unscoped.select(1)
+ .joins(:project)
+ .where('issues.id = issue_assignees.issue_id AND projects.namespace_id = ?', member.source_id)
+
+ # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
+ IssueAssignee.unscoped
+ .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
+ .delete_all
+
+ MergeRequestsFinder.new(current_user, group_id: member.source_id, assignee_id: member.user_id)
+ .execute
+ .update_all(assignee_id: nil)
+ else
+ project = member.source
+
+ # SELECT 1 FROM issues WHERE issues.id = issue_assignees.issue_id AND issues.project_id = X
+ issues = Issue.unscoped.select(1)
+ .where('issues.id = issue_assignees.issue_id')
+ .where(project_id: project.id)
+
+ # DELETE FROM issue_assignees WHERE user_id = X AND EXISTS (...)
+ IssueAssignee.unscoped
+ .where('user_id = :user_id AND EXISTS (:sub)', user_id: member.user_id, sub: issues)
+ .delete_all
+
+ project.merge_requests.opened.assigned_to(member.user).update_all(assignee_id: nil)
+ end
+
+ member.user.invalidate_cache_counts
+ end
end
end
diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb
index 2614153d900..24293b30005 100644
--- a/app/services/members/request_access_service.rb
+++ b/app/services/members/request_access_service.rb
@@ -1,13 +1,6 @@
module Members
- class RequestAccessService < BaseService
- attr_accessor :source
-
- def initialize(source, current_user)
- @source = source
- @current_user = current_user
- end
-
- def execute
+ class RequestAccessService < Members::BaseService
+ def execute(source)
raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
source.members.create(
@@ -19,7 +12,7 @@ module Members
private
def can_request_access?(source)
- source && can?(current_user, :request_access, source)
+ can?(current_user, :request_access, source)
end
end
end
diff --git a/app/services/members/update_service.rb b/app/services/members/update_service.rb
new file mode 100644
index 00000000000..48b3d59f7bd
--- /dev/null
+++ b/app/services/members/update_service.rb
@@ -0,0 +1,16 @@
+module Members
+ class UpdateService < Members::BaseService
+ # returns the updated member
+ def execute(member, permission: :update)
+ raise Gitlab::Access::AccessDeniedError unless can?(current_user, action_member_permission(permission, member), member)
+
+ old_access_level = member.human_access
+
+ if member.update_attributes(params)
+ after_execute(action: permission, old_access_level: old_access_level, member: member)
+ end
+
+ member
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index 20a2b50d3de..23262b62615 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -24,6 +24,17 @@ module MergeRequests
private
+ def handle_wip_event(merge_request)
+ if wip_event = params.delete(:wip_event)
+ # We update the title that is provided in the params or we use the mr title
+ title = params[:title] || merge_request.title
+ params[:title] = case wip_event
+ when 'wip' then MergeRequest.wip_title(title)
+ when 'unwip' then MergeRequest.wipless_title(title)
+ end
+ end
+ end
+
def merge_request_metrics_service(merge_request)
MergeRequestMetricsService.new(merge_request.metrics)
end
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index a18b1c90765..c57a2445341 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -34,6 +34,12 @@ module MergeRequests
super
end
+ # Override from IssuableBaseService
+ def handle_quick_actions_on_create(merge_request)
+ super
+ handle_wip_event(merge_request)
+ end
+
private
def update_merge_requests_head_pipeline(merge_request)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index c153872c874..8a40ad88182 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -98,17 +98,6 @@ module MergeRequests
private
- def handle_wip_event(merge_request)
- if wip_event = params.delete(:wip_event)
- # We update the title that is provided in the params or we use the mr title
- title = params[:title] || merge_request.title
- params[:title] = case wip_event
- when 'wip' then MergeRequest.wip_title(title)
- when 'unwip' then MergeRequest.wipless_title(title)
- end
- end
- end
-
def create_branch_change_note(issuable, branch_type, old_branch, new_branch)
SystemNoteService.change_branch(
issuable, issuable.project, current_user, branch_type,
diff --git a/app/services/notes/quick_actions_service.rb b/app/services/notes/quick_actions_service.rb
index a8d0cc15527..0a33d5f3f3d 100644
--- a/app/services/notes/quick_actions_service.rb
+++ b/app/services/notes/quick_actions_service.rb
@@ -9,14 +9,12 @@ module Notes
UPDATE_SERVICES[note.noteable_type]
end
- def self.supported?(note, current_user)
- noteable_update_service(note) &&
- current_user &&
- current_user.can?(:"update_#{note.to_ability_name}", note.noteable)
+ def self.supported?(note)
+ !!noteable_update_service(note)
end
def supported?(note)
- self.class.supported?(note, current_user)
+ self.class.supported?(note)
end
def extract_commands(note, options = {})
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index c760bd3b626..00fdd047208 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -1,5 +1,8 @@
module Projects
class UpdatePagesService < BaseService
+ InvaildStateError = Class.new(StandardError)
+ FailedToExtractError = Class.new(StandardError)
+
BLOCK_SIZE = 32.kilobytes
MAX_SIZE = 1.terabyte
SITE_PATH = 'public/'.freeze
@@ -11,13 +14,15 @@ module Projects
end
def execute
+ register_attempt
+
# Create status notifying the deployment of pages
@status = create_status
@status.enqueue!
@status.run!
- raise 'missing pages artifacts' unless build.artifacts?
- raise 'pages are outdated' unless latest?
+ raise InvaildStateError, 'missing pages artifacts' unless build.artifacts?
+ raise InvaildStateError, 'pages are outdated' unless latest?
# Create temporary directory in which we will extract the artifacts
FileUtils.mkdir_p(tmp_path)
@@ -26,24 +31,22 @@ module Projects
# Check if we did extract public directory
archive_public_path = File.join(archive_path, 'public')
- raise 'pages miss the public folder' unless Dir.exist?(archive_public_path)
- raise 'pages are outdated' unless latest?
+ raise FailedToExtractError, 'pages miss the public folder' unless Dir.exist?(archive_public_path)
+ raise InvaildStateError, 'pages are outdated' unless latest?
deploy_page!(archive_public_path)
success
end
- rescue => e
+ rescue InvaildStateError, FailedToExtractError => e
register_failure
error(e.message)
- ensure
- register_attempt
- build.erase_artifacts! unless build.has_expiring_artifacts?
end
private
def success
@status.success
+ delete_artifact!
super
end
@@ -52,6 +55,7 @@ module Projects
@status.allow_failure = !latest?
@status.description = message
@status.drop(:script_failure)
+ delete_artifact!
super
end
@@ -72,7 +76,7 @@ module Projects
elsif artifacts.ends_with?('.zip')
extract_zip_archive!(temp_path)
else
- raise 'unsupported artifacts format'
+ raise FailedToExtractError, 'unsupported artifacts format'
end
end
@@ -81,17 +85,17 @@ module Projects
%W(dd bs=#{BLOCK_SIZE} count=#{blocks}),
%W(tar -x -C #{temp_path} #{SITE_PATH}),
err: '/dev/null')
- raise 'pages failed to extract' unless results.compact.all?(&:success?)
+ raise FailedToExtractError, 'pages failed to extract' unless results.compact.all?(&:success?)
end
def extract_zip_archive!(temp_path)
- raise 'missing artifacts metadata' unless build.artifacts_metadata?
+ raise FailedToExtractError, 'missing artifacts metadata' unless build.artifacts_metadata?
# Calculate page size after extract
public_entry = build.artifacts_metadata_entry(SITE_PATH, recursive: true)
if public_entry.total_size > max_size
- raise "artifacts for pages are too large: #{public_entry.total_size}"
+ raise FailedToExtractError, "artifacts for pages are too large: #{public_entry.total_size}"
end
# Requires UnZip at least 6.00 Info-ZIP.
@@ -100,7 +104,7 @@ module Projects
# We add * to end of SITE_PATH, because we want to extract SITE_PATH and all subdirectories
site_path = File.join(SITE_PATH, '*')
unless system(*%W(unzip -qq -n #{artifacts} #{site_path} -d #{temp_path}))
- raise 'pages failed to extract'
+ raise FailedToExtractError, 'pages failed to extract'
end
end
@@ -163,6 +167,11 @@ module Projects
build.artifacts_file.path
end
+ def delete_artifact!
+ build.reload # Reload stable object to prevent erase artifacts with old state
+ build.erase_artifacts! unless build.has_expiring_artifacts?
+ end
+
def latest_sha
project.commit(build.ref).try(:sha).to_s
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 0e235a6d2a0..379a8068023 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -15,6 +15,8 @@ module Projects
return error("Could not set the default branch") unless project.change_head(params[:default_branch])
end
+ ensure_wiki_exists if enabling_wiki?
+
if project.update_attributes(params.except(:default_branch))
if project.previous_changes.include?('path')
project.rename_repo
@@ -52,5 +54,18 @@ module Projects
project.repository.exists? &&
new_branch && new_branch != project.default_branch
end
+
+ def enabling_wiki?
+ return false if @project.wiki_enabled?
+
+ params[:project_feature_attributes][:wiki_access_level].to_i > ProjectFeature::DISABLED
+ end
+
+ def ensure_wiki_exists
+ ProjectWiki.new(project, project.owner).wiki
+ rescue ProjectWiki::CouldNotCreateWikiError
+ log_error("Could not create wiki for #{project.full_name}")
+ Gitlab::Metrics.counter(:wiki_can_not_be_created_total, 'Counts the times we failed to create a wiki')
+ end
end
end
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 1e9bd84e749..cba49faac31 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -347,9 +347,9 @@ module QuickActions
"#{verb} this #{noun} as Work In Progress."
end
condition do
- issuable.persisted? &&
- issuable.respond_to?(:work_in_progress?) &&
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ issuable.respond_to?(:work_in_progress?) &&
+ # Allow it to mark as WIP on MR creation page _or_ through MR notes.
+ (issuable.new_record? || current_user.can?(:"update_#{issuable.to_ability_name}", issuable))
end
command :wip do
@updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index a6b7a6e1416..ba7946fd23c 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -11,6 +11,8 @@ class SystemHooksService
SystemHook.hooks_for(hooks_scope).find_each do |hook|
hook.async_execute(data, 'system_hooks')
end
+
+ Gitlab::Plugin.execute_all_async(data)
end
private
@@ -18,8 +20,8 @@ class SystemHooksService
def build_event_data(model, event)
data = {
event_name: build_event_name(model, event),
- created_at: model.created_at.xmlschema,
- updated_at: model.updated_at.xmlschema
+ created_at: model.created_at&.xmlschema,
+ updated_at: model.updated_at&.xmlschema
}
case model
diff --git a/app/validators/url_placeholder_validator.rb b/app/validators/url_placeholder_validator.rb
new file mode 100644
index 00000000000..dd681218b6b
--- /dev/null
+++ b/app/validators/url_placeholder_validator.rb
@@ -0,0 +1,32 @@
+# UrlValidator
+#
+# Custom validator for URLs.
+#
+# By default, only URLs for the HTTP(S) protocols will be considered valid.
+# Provide a `:protocols` option to configure accepted protocols.
+#
+# Also, this validator can help you validate urls with placeholders inside.
+# Usually, if you have a url like 'http://www.example.com/%{project_path}' the
+# URI parser will reject that URL format. Provide a `:placeholder_regex` option
+# to configure accepted placeholders.
+#
+# Example:
+#
+# class User < ActiveRecord::Base
+# validates :personal_url, url: true
+#
+# validates :ftp_url, url: { protocols: %w(ftp) }
+#
+# validates :git_url, url: { protocols: %w(http https ssh git) }
+#
+# validates :placeholder_url, url: { placeholder_regex: /(project_path|project_id|default_branch)/ }
+# end
+#
+class UrlPlaceholderValidator < UrlValidator
+ def validate_each(record, attribute, value)
+ placeholder_regex = self.options[:placeholder_regex]
+ value = value.gsub(/%{#{placeholder_regex}}/, 'foo') if placeholder_regex && value
+
+ super(record, attribute, value)
+ end
+end
diff --git a/app/validators/variable_duplicates_validator.rb b/app/validators/variable_duplicates_validator.rb
index 4bfa3c45303..72660be6c43 100644
--- a/app/validators/variable_duplicates_validator.rb
+++ b/app/validators/variable_duplicates_validator.rb
@@ -5,6 +5,8 @@
# - Use `validates :xxx, uniqueness: { scope: :xxx_id }` in a child model
class VariableDuplicatesValidator < ActiveModel::EachValidator
def validate_each(record, attribute, value)
+ return if record.errors.include?(:"#{attribute}.key")
+
if options[:scope]
scoped = value.group_by do |variable|
Array(options[:scope]).map { |attr| variable.send(attr) } # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index 20527d31870..68788134b8e 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -173,7 +173,7 @@
Password authentication enabled for Git over HTTP(S)
.help-block
When disabled, a Personal Access Token
- - if Gitlab::LDAP::Config.enabled?
+ - if Gitlab::Auth::LDAP::Config.enabled?
or LDAP password
must be used to authenticate.
- if omniauth_enabled? && button_based_providers.any?
@@ -666,15 +666,15 @@
.checkbox
= f.label :usage_ping_enabled do
= f.check_box :usage_ping_enabled, disabled: !can_be_configured
- Usage ping enabled
- = link_to icon('question-circle'), help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ Enable usage ping
.help-block
- if can_be_configured
- Every week GitLab will report license usage back to GitLab, Inc.
- Disable this option if you do not want this to occur. To see the
- JSON payload that will be sent, visit the
- = succeed '.' do
- = link_to "Cohorts page", admin_cohorts_path(anchor: 'usage-ping')
+ To help improve GitLab and its user experience, GitLab will
+ periodically collect usage information.
+ = link_to 'Learn more', help_page_path("user/admin_area/settings/usage_statistics", anchor: "usage-ping")
+ about what information is shared with GitLab Inc. Visit
+ = link_to 'Cohorts', admin_cohorts_path(anchor: 'usage-ping')
+ to see the JSON payload sent.
- else
The usage ping is disabled, and cannot be configured through this
form. For more information, see the documentation on
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e3711421b61..05c41082882 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -164,7 +164,7 @@
%h4 Latest projects
- @projects.each do |project|
%p
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project], class: 'str-truncated-60'
%span.light.pull-right
#{time_ago_with_tooltip(project.created_at)}
.col-md-4
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 2545cecc721..324f3c0a22f 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -68,7 +68,7 @@
- @projects.each do |project|
%li
%strong
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
@@ -86,7 +86,7 @@
- @group.shared_projects.sort_by(&:name).each do |project|
%li
%strong
- = link_to project.name_with_namespace, [:admin, project.namespace.becomes(Namespace), project]
+ = link_to project.full_name, [:admin, project.namespace.becomes(Namespace), project]
%span.badge
= storage_counter(project.statistics.storage_size)
%span.pull-right.light
diff --git a/app/views/admin/identities/_form.html.haml b/app/views/admin/identities/_form.html.haml
index 112a201fafa..5381b854f5c 100644
--- a/app/views/admin/identities/_form.html.haml
+++ b/app/views/admin/identities/_form.html.haml
@@ -4,7 +4,7 @@
.form-group
= f.label :provider, class: 'control-label'
.col-sm-10
- - values = Gitlab::OAuth::Provider.providers.map { |name| ["#{Gitlab::OAuth::Provider.label_for(name)} (#{name})", name] }
+ - values = Gitlab::Auth::OAuth::Provider.providers.map { |name| ["#{Gitlab::Auth::OAuth::Provider.label_for(name)} (#{name})", name] }
= f.select :provider, values, { allow_blank: false }, class: 'form-control'
.form-group
= f.label :extern_uid, "Identifier", class: 'control-label'
diff --git a/app/views/admin/identities/_identity.html.haml b/app/views/admin/identities/_identity.html.haml
index 8c658905bd6..ef5a3f1d969 100644
--- a/app/views/admin/identities/_identity.html.haml
+++ b/app/views/admin/identities/_identity.html.haml
@@ -1,6 +1,6 @@
%tr
%td
- #{Gitlab::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
+ #{Gitlab::Auth::OAuth::Provider.label_for(identity.provider)} (#{identity.provider})
%td
= identity.extern_uid
%td
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 42f92079d85..c02ddafe108 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -1,8 +1,8 @@
- add_to_breadcrumbs "Projects", admin_projects_path
-- breadcrumb_title @project.name_with_namespace
-- page_title @project.name_with_namespace, "Projects"
+- breadcrumb_title @project.full_name
+- page_title @project.full_name, "Projects"
%h3.page-title
- Project: #{@project.name_with_namespace}
+ Project: #{@project.full_name}
= link_to edit_project_path(@project), class: "btn btn-nr pull-right" do
%i.fa.fa-pencil-square-o
Edit
diff --git a/app/views/admin/runners/_runner.html.haml b/app/views/admin/runners/_runner.html.haml
index 140688b52d3..e1cee584929 100644
--- a/app/views/admin/runners/_runner.html.haml
+++ b/app/views/admin/runners/_runner.html.haml
@@ -17,6 +17,8 @@
%td
= runner.version
%td
+ = runner.ip_address
+ %td
- if runner.shared?
n/a
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index abec3607cab..9f13dbbbd82 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -60,6 +60,7 @@
%th Runner token
%th Description
%th Version
+ %th IP Address
%th Projects
%th Jobs
%th Tags
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 6d8fad0eb8d..185e9d7b35d 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -39,7 +39,7 @@
%tr.alert-info
%td
%strong
- = project.name_with_namespace
+ = project.full_name
%td
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
@@ -61,7 +61,7 @@
- @projects.each do |project|
%tr
%td
- = project.name_with_namespace
+ = project.full_name
%td
.pull-right
= form_for [:admin, project.namespace.becomes(Namespace), project, project.runner_projects.new] do |f|
@@ -95,7 +95,7 @@
%td.status
- if project
- = project.name_with_namespace
+ = project.full_name
%td.build-link
- if project
diff --git a/app/views/admin/users/projects.html.haml b/app/views/admin/users/projects.html.haml
index 4a440f3f6d4..96835ee9af5 100644
--- a/app/views/admin/users/projects.html.haml
+++ b/app/views/admin/users/projects.html.haml
@@ -29,12 +29,12 @@
.panel.panel-default
.panel-heading Joined projects (#{@joined_projects.count})
%ul.well-list
- - @joined_projects.sort_by(&:name_with_namespace).each do |project|
+ - @joined_projects.sort_by(&:full_name).each do |project|
- member = project.team.find_member(@user.id)
%li.project_member
.list-item-name
= link_to admin_project_path(project), class: dom_class(project) do
- = project.name_with_namespace
+ = project.full_name
- if member
.pull-right
diff --git a/app/views/devise/sessions/two_factor.html.haml b/app/views/devise/sessions/two_factor.html.haml
index 56ec1b3db0d..6e54b9b5645 100644
--- a/app/views/devise/sessions/two_factor.html.haml
+++ b/app/views/devise/sessions/two_factor.html.haml
@@ -1,7 +1,3 @@
-- if inject_u2f_api?
- - content_for :page_specific_javascripts do
- = webpack_bundle_tag('u2f')
-
%div
= render 'devise/shared/tab_single', tab_title: 'Two-Factor Authentication'
.login-box
diff --git a/app/views/groups/group_members/update.js.haml b/app/views/groups/group_members/update.js.haml
deleted file mode 100644
index 9d05bff6c4e..00000000000
--- a/app/views/groups/group_members/update.js.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-:plain
- var $listItem = $('#{escape_javascript(render('shared/members/member', member: @group_member))}');
- $("##{dom_id(@group_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
- gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@group_member)}"));
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index f2ae7c52031..ca3f018c5e6 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -1,12 +1,13 @@
- page_title "Issues"
-- group_issues_exists = group_issues(@group).exists?
= content_for :meta_tags do
= auto_discovery_link_tag(:atom, params.merge(rss_url_options), title: "#{@group.name} issues")
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
-- if group_issues_exists
+- if group_issues_count(state: 'all').zero?
+ = render 'shared/empty_states/issues', project_select_button: true
+- else
.top-area
= render 'shared/issuable/nav', type: :issues
.nav-controls
@@ -19,5 +20,3 @@
= render 'shared/issuable/search_bar', type: :issues
= render 'shared/issues'
-- else
- = render 'shared/empty_states/issues', project_select_button: true
diff --git a/app/views/groups/merge_requests.html.haml b/app/views/groups/merge_requests.html.haml
index 046b92bd9fb..4ccd16f3e11 100644
--- a/app/views/groups/merge_requests.html.haml
+++ b/app/views/groups/merge_requests.html.haml
@@ -1,9 +1,6 @@
- page_title "Merge Requests"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
-
-- if @group_merge_requests.empty?
+- if group_merge_requests_count(state: 'all').zero?
= render 'shared/empty_states/merge_requests', project_select_button: true
- else
.top-area
diff --git a/app/views/groups/projects.html.haml b/app/views/groups/projects.html.haml
index 8d2bc810a7d..ef181b425bc 100644
--- a/app/views/groups/projects.html.haml
+++ b/app/views/groups/projects.html.haml
@@ -14,7 +14,7 @@
.list-item-name
%span{ class: visibility_level_color(project.visibility_level) }
= visibility_level_icon(project.visibility_level)
- %strong= link_to project.name_with_namespace, project
+ %strong= link_to project.full_name, project
.pull-right
- if project.archived
%span.label.label-warning archived
diff --git a/app/views/ide/index.html.haml b/app/views/ide/index.html.haml
deleted file mode 100644
index 3dbdfc97654..00000000000
--- a/app/views/ide/index.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- @body_class = 'ide'
-- page_title 'IDE'
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'ide', force_same_domain: true
-
-#ide.ide-loading{ data: {"empty-state-svg-path" => image_path('illustrations/multi_file_editor_empty.svg')} }
- .text-center
- = icon('spinner spin 2x')
- %h2.clgray= _('Loading the GitLab IDE...')
diff --git a/app/views/invites/show.html.haml b/app/views/invites/show.html.haml
index ad6213b4efd..c2bb1216c5f 100644
--- a/app/views/invites/show.html.haml
+++ b/app/views/invites/show.html.haml
@@ -12,7 +12,7 @@
- project = @member.source
project
%strong
- = link_to project.name_with_namespace, project_url(project)
+ = link_to project.full_name, project_url(project)
- when Group
- group = @member.source
group
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 0c979109b3f..b981b5fdafa 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -42,7 +42,6 @@
= webpack_bundle_tag "common"
= webpack_bundle_tag "main"
= webpack_bundle_tag "raven" if Gitlab::CurrentSettings.clientside_sentry_enabled
- = webpack_bundle_tag "test" if Rails.env.test?
- if content_for?(:page_specific_javascripts)
= yield :page_specific_javascripts
diff --git a/app/views/layouts/nav/projects_dropdown/_show.html.haml b/app/views/layouts/nav/projects_dropdown/_show.html.haml
index 59becb043d3..5809d6f7fea 100644
--- a/app/views/layouts/nav/projects_dropdown/_show.html.haml
+++ b/app/views/layouts/nav/projects_dropdown/_show.html.haml
@@ -1,4 +1,4 @@
-- project_meta = { id: @project.id, name: @project.name, namespace: @project.name_with_namespace, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
+- project_meta = { id: @project.id, name: @project.name, namespace: @project.full_name, web_url: project_path(@project), avatar_url: @project.avatar_url } if @project&.persisted?
.projects-dropdown-container
.project-dropdown-sidebar.qa-projects-dropdown-sidebar
%ul
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index 47ae79b7a69..b520f28123f 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,6 +1,5 @@
-- issues_count = IssuesFinder.new(current_user, group_id: @group.id, state: 'opened').execute.count
-- merge_requests_count = MergeRequestsFinder.new(current_user, group_id: @group.id, state: 'opened', non_archived: true).execute.count
-
+- issues_count = group_issues_count(state: 'opened')
+- merge_requests_count = group_merge_requests_count(state: 'opened')
- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
diff --git a/app/views/layouts/project.html.haml b/app/views/layouts/project.html.haml
index 6b847fb4b7c..6b51483810e 100644
--- a/app/views/layouts/project.html.haml
+++ b/app/views/layouts/project.html.haml
@@ -1,4 +1,4 @@
-- page_title @project.name_with_namespace
+- page_title @project.full_name
- page_description @project.description unless page_description
- header_title project_title(@project) unless header_title
- nav "project"
diff --git a/app/views/notify/project_was_exported_email.html.haml b/app/views/notify/project_was_exported_email.html.haml
index f0ba7827cef..71c62f6be4e 100644
--- a/app/views/notify/project_was_exported_email.html.haml
+++ b/app/views/notify/project_was_exported_email.html.haml
@@ -3,6 +3,6 @@
%p
The project export can be downloaded from:
= link_to download_export_project_url(@project), rel: 'nofollow', download: '' do
- = @project.name_with_namespace + " export"
+ = @project.full_name + " export"
%p
The download link will expire in 24 hours.
diff --git a/app/views/notify/project_was_moved_email.html.haml b/app/views/notify/project_was_moved_email.html.haml
index c476a39b661..1b6b1a81665 100644
--- a/app/views/notify/project_was_moved_email.html.haml
+++ b/app/views/notify/project_was_moved_email.html.haml
@@ -3,7 +3,7 @@
%p
The project is now located under
= link_to project_url(@project) do
- = @project.name_with_namespace
+ = @project.full_name
%p
To update the remote url in your local repository run (for ssh):
%p{ style: "background: #f5f5f5; padding:10px; border:1px solid #ddd" }
diff --git a/app/views/profiles/_head.html.haml b/app/views/profiles/_head.html.haml
deleted file mode 100644
index a8eb66ca13c..00000000000
--- a/app/views/profiles/_head.html.haml
+++ /dev/null
@@ -1,2 +0,0 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('profile')
diff --git a/app/views/profiles/accounts/show.html.haml b/app/views/profiles/accounts/show.html.haml
index 0f849f6f8b7..02263095599 100644
--- a/app/views/profiles/accounts/show.html.haml
+++ b/app/views/profiles/accounts/show.html.haml
@@ -1,6 +1,5 @@
- page_title "Account"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
- if current_user.ldap_user?
.alert.alert-info
diff --git a/app/views/profiles/audit_log.html.haml b/app/views/profiles/audit_log.html.haml
index cbea5ca605a..a924369050b 100644
--- a/app/views/profiles/audit_log.html.haml
+++ b/app/views/profiles/audit_log.html.haml
@@ -1,6 +1,5 @@
- page_title "Authentication log"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/chat_names/_chat_name.html.haml b/app/views/profiles/chat_names/_chat_name.html.haml
index fe1cf802971..c7094800fb2 100644
--- a/app/views/profiles/chat_names/_chat_name.html.haml
+++ b/app/views/profiles/chat_names/_chat_name.html.haml
@@ -4,7 +4,7 @@
%td
%strong
- if can?(current_user, :read_project, project)
- = link_to project.name_with_namespace, project_path(project)
+ = link_to project.full_name, project_path(project)
- else
.light N/A
%td
diff --git a/app/views/profiles/chat_names/index.html.haml b/app/views/profiles/chat_names/index.html.haml
index 8f7121afe02..4b6e419af50 100644
--- a/app/views/profiles/chat_names/index.html.haml
+++ b/app/views/profiles/chat_names/index.html.haml
@@ -1,6 +1,5 @@
- page_title 'Chat'
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/emails/index.html.haml b/app/views/profiles/emails/index.html.haml
index df1df4f5d72..e3c2bd1150e 100644
--- a/app/views/profiles/emails/index.html.haml
+++ b/app/views/profiles/emails/index.html.haml
@@ -1,6 +1,5 @@
- page_title "Emails"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/gpg_keys/index.html.haml b/app/views/profiles/gpg_keys/index.html.haml
index e44506ec9c9..1d2e41cb437 100644
--- a/app/views/profiles/gpg_keys/index.html.haml
+++ b/app/views/profiles/gpg_keys/index.html.haml
@@ -1,6 +1,5 @@
- page_title "GPG Keys"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/keys/index.html.haml b/app/views/profiles/keys/index.html.haml
index 5f7b41cf30e..1e206def7ee 100644
--- a/app/views/profiles/keys/index.html.haml
+++ b/app/views/profiles/keys/index.html.haml
@@ -1,6 +1,5 @@
- page_title "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
@@ -13,7 +12,9 @@
Add an SSH key
%p.profile-settings-content
Before you can add an SSH key you need to
- = link_to "generate it.", help_page_path("ssh/README")
+ = link_to "generate one", help_page_path("ssh/README", anchor: 'generating-a-new-ssh-key-pair')
+ or use an
+ = link_to "existing key.", help_page_path("ssh/README", anchor: 'locating-an-existing-ssh-key-pair')
= render 'form'
%hr
%h5
diff --git a/app/views/profiles/keys/show.html.haml b/app/views/profiles/keys/show.html.haml
index 7b7960708c4..28be6172219 100644
--- a/app/views/profiles/keys/show.html.haml
+++ b/app/views/profiles/keys/show.html.haml
@@ -2,5 +2,4 @@
- breadcrumb_title @key.title
- page_title @key.title, "SSH Keys"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
= render "key_details"
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index 202eccb7bb6..8f099aa6dd7 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -1,6 +1,5 @@
- page_title "Notifications"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
%div
- if @user.errors.any?
diff --git a/app/views/profiles/personal_access_tokens/index.html.haml b/app/views/profiles/personal_access_tokens/index.html.haml
index f445e5a2417..78848542810 100644
--- a/app/views/profiles/personal_access_tokens/index.html.haml
+++ b/app/views/profiles/personal_access_tokens/index.html.haml
@@ -2,7 +2,6 @@
- page_title "Personal Access Tokens"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
.row.prepend-top-default
.col-lg-4.profile-settings-sidebar
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 66d1d1e8d44..6aefd97bb96 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -1,6 +1,5 @@
- page_title 'Preferences'
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
.col-lg-4.application-theme
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 110736dc557..e497eab32e0 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -1,6 +1,5 @@
- breadcrumb_title "Edit Profile"
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user prepend-top-default js-quick-submit' }, authenticity_token: true do |f|
= form_errors(@user)
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index e58cd20402c..1bd10018b40 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -2,13 +2,6 @@
- add_to_breadcrumbs("Two-Factor Authentication", profile_account_path)
- @content_class = "limit-container-width" unless fluid_layout
-= render 'profiles/head'
-
-- content_for :page_specific_javascripts do
- - if inject_u2f_api?
- = webpack_bundle_tag('u2f')
- = webpack_bundle_tag('two_factor_auth')
-
.js-two-factor-auth{ 'data-two-factor-skippable' => "#{two_factor_skippable?}", 'data-two_factor_skip_url' => skip_profile_two_factor_auth_path }
.row.prepend-top-default
.col-lg-4
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index b565f14747a..a2ecfddb163 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -23,6 +23,12 @@
- deleted_message = s_('ForkedFromProjectPath|Forked from %{project_name} (deleted)')
= deleted_message % { project_name: fork_source_name(@project) }
+ .project-badges
+ - @project.badges.each do |badge|
+ - badge_link_url = badge.rendered_link_url(@project)
+ %a{ href: badge_link_url, target: '_blank', rel: 'noopener noreferrer' }
+ %img{ src: badge.rendered_image_url(@project), alt: badge_link_url }
+
.project-repo-buttons
.count-buttons
= render 'projects/buttons/star'
diff --git a/app/views/projects/_issuable_by_email.html.haml b/app/views/projects/_issuable_by_email.html.haml
index 749e273b2e2..c137e38ed50 100644
--- a/app/views/projects/_issuable_by_email.html.haml
+++ b/app/views/projects/_issuable_by_email.html.haml
@@ -18,7 +18,14 @@
.email-modal-input-group.input-group
= text_field_tag :issuable_email, email, class: "monospace js-select-on-focus form-control", readonly: true
.input-group-btn
- = clipboard_button(target: '#issuable_email')
+ = clipboard_button(target: '#issuable_email', class: 'btn btn-clipboard btn-transparent hidden-xs')
+ = mail_to email, class: 'btn btn-clipboard btn-transparent',
+ subject: _("Enter the #{name} title"),
+ body: _("Enter the #{name} description"),
+ title: _('Send email'),
+ data: { toggle: 'tooltip', placement: 'bottom' } do
+ = sprite_icon('mail')
+
%p
= render 'by_email_description'
%p
diff --git a/app/views/projects/_merge_request_fast_forward_settings.html.haml b/app/views/projects/_merge_request_fast_forward_settings.html.haml
index 8129c72feb2..f455522d17c 100644
--- a/app/views/projects/_merge_request_fast_forward_settings.html.haml
+++ b/app/views/projects/_merge_request_fast_forward_settings.html.haml
@@ -3,7 +3,7 @@
.radio
= label_tag :project_merge_method_ff do
- = form.radio_button :merge_method, :ff, class: "js-merge-method-radio"
+ = form.radio_button :merge_method, :ff, class: "js-merge-method-radio qa-radio-button-merge-ff"
%strong Fast-forward merge
%br
%span.descr
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index d367bd6be7b..f4b5ef1555e 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -1,6 +1,8 @@
- visibility_level = params.dig(:project, :visibility_level) || default_project_visibility
+- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
.row{ id: project_name_id }
+ = f.hidden_field :ci_cd_only, value: ci_cd_only
.form-group.project-path.col-sm-6
= f.label :namespace_id, class: 'label-light' do
%span
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 2a77dedd9a2..f93bb02acb9 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -11,8 +11,7 @@
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
- = edit_blob_link
- = ide_blob_link
+ = edit_blob_button
- if current_user
= replace_blob_link
= delete_blob_link
diff --git a/app/views/projects/blob/_upload.html.haml b/app/views/projects/blob/_upload.html.haml
index b3afd16f900..f1324c61500 100644
--- a/app/views/projects/blob/_upload.html.haml
+++ b/app/views/projects/blob/_upload.html.haml
@@ -27,6 +27,3 @@
- unless can?(current_user, :push_code, @project)
.inline.prepend-left-10
= commit_in_fork_help
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('blob')
diff --git a/app/views/projects/blob/_viewer.html.haml b/app/views/projects/blob/_viewer.html.haml
index cc85e5de40f..3124443b4e4 100644
--- a/app/views/projects/blob/_viewer.html.haml
+++ b/app/views/projects/blob/_viewer.html.haml
@@ -1,9 +1,10 @@
- hidden = local_assigns.fetch(:hidden, false)
- render_error = viewer.render_error
+- rich_type = viewer.type == :rich ? viewer.partial_name : nil
- load_async = local_assigns.fetch(:load_async, viewer.load_async? && render_error.nil?)
- viewer_url = local_assigns.fetch(:viewer_url) { url_for(params.merge(viewer: viewer.type, format: :json)) } if load_async
-.blob-viewer{ data: { type: viewer.type, url: viewer_url }, class: ('hidden' if hidden) }
+.blob-viewer{ data: { type: viewer.type, rich_type: rich_type, url: viewer_url }, class: ('hidden' if hidden) }
- if render_error
= render 'projects/blob/render_error', viewer: viewer
- elsif load_async
diff --git a/app/views/projects/blob/viewers/_balsamiq.html.haml b/app/views/projects/blob/viewers/_balsamiq.html.haml
index 15349387eb2..b20106e8c3a 100644
--- a/app/views/projects/blob/viewers/_balsamiq.html.haml
+++ b/app/views/projects/blob/viewers/_balsamiq.html.haml
@@ -1,4 +1 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('balsamiq_viewer')
-
.file-content.balsamiq-viewer#js-balsamiq-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_notebook.html.haml b/app/views/projects/blob/viewers/_notebook.html.haml
index d1ffaca35b9..eb4ca1b9816 100644
--- a/app/views/projects/blob/viewers/_notebook.html.haml
+++ b/app/views/projects/blob/viewers/_notebook.html.haml
@@ -1,5 +1 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('notebook_viewer')
-
.file-content#js-notebook-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_pdf.html.haml b/app/views/projects/blob/viewers/_pdf.html.haml
index fc3f0d922b1..95d837a57dc 100644
--- a/app/views/projects/blob/viewers/_pdf.html.haml
+++ b/app/views/projects/blob/viewers/_pdf.html.haml
@@ -1,5 +1 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pdf_viewer')
-
.file-content#js-pdf-viewer{ data: { endpoint: blob_raw_path } }
diff --git a/app/views/projects/blob/viewers/_sketch.html.haml b/app/views/projects/blob/viewers/_sketch.html.haml
index 8fb67c819c1..b4b6492b92f 100644
--- a/app/views/projects/blob/viewers/_sketch.html.haml
+++ b/app/views/projects/blob/viewers/_sketch.html.haml
@@ -1,7 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('sketch_viewer')
-
.file-content#js-sketch-viewer{ data: { endpoint: blob_raw_path } }
.js-loading-icon.text-center.prepend-top-default.append-bottom-default.js-loading-icon{ 'aria-label' => 'Loading Sketch preview' }
= icon('spinner spin 2x', 'aria-hidden' => 'true');
diff --git a/app/views/projects/blob/viewers/_stl.html.haml b/app/views/projects/blob/viewers/_stl.html.haml
index e58809ec008..55dd8cba7fe 100644
--- a/app/views/projects/blob/viewers/_stl.html.haml
+++ b/app/views/projects/blob/viewers/_stl.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('stl_viewer')
-
.file-content.is-stl-loading
.text-center#js-stl-viewer{ data: { endpoint: blob_raw_path } }
= icon('spinner spin 2x', class: 'prepend-top-default append-bottom-default', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
diff --git a/app/views/projects/branches/_panel.html.haml b/app/views/projects/branches/_panel.html.haml
new file mode 100644
index 00000000000..12e5a8e8d69
--- /dev/null
+++ b/app/views/projects/branches/_panel.html.haml
@@ -0,0 +1,19 @@
+- branches = local_assigns.fetch(:branches)
+- state = local_assigns.fetch(:state)
+- panel_title = local_assigns.fetch(:panel_title)
+- show_more_text = local_assigns.fetch(:show_more_text)
+- project = local_assigns.fetch(:project)
+- overview_max_branches = local_assigns.fetch(:overview_max_branches)
+
+- return unless branches.any?
+
+.panel.panel-default.prepend-top-10
+ .panel-heading
+ %h4.panel-title
+ = panel_title
+ %ul.content-list.all-branches
+ - branches.first(overview_max_branches).each do |branch|
+ = render "projects/branches/branch", branch: branch, merged: project.repository.merged_to_root_ref?(branch)
+ - if branches.size > overview_max_branches
+ .panel-footer.text-center
+ = link_to show_more_text, project_branches_filtered_path(project, state: state), id: "state-#{state}", data: { state: state }
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index fb770764364..5dcc72d8263 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -3,26 +3,35 @@
%div{ class: container_class }
.top-area.adjust
- - if can?(current_user, :admin_project, @project)
- .nav-text
- - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
- = s_('Branches|Protected branches can be managed in %{project_settings_link}').html_safe % { project_settings_link: project_settings_link }
+ %ul.nav-links.issues-state-filters
+ %li{ class: active_when(@mode == 'overview') }>
+ = link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches')
+
+ %li{ class: active_when(@mode == 'active') }>
+ = link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), title: s_('Branches|Show active branches')
+
+ %li{ class: active_when(@mode == 'stale') }>
+ = link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), title: s_('Branches|Show stale branches')
+
+ %li{ class: active_when(!%w[overview active stale].include?(@mode)) }>
+ = link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches')
.nav-controls
- = form_tag(filter_branches_path, method: :get) do
+ = form_tag(project_branches_filtered_path(@project, state: 'all'), method: :get) do
= search_field_tag :search, params[:search], { placeholder: s_('Branches|Filter by branch name'), id: 'branch-search', class: 'form-control search-text-input input-short', spellcheck: false }
- .dropdown.inline>
- %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
- %span.light
- = branches_sort_options_hash[@sort]
- = icon('chevron-down')
- %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
- %li.dropdown-header
- = s_('Branches|Sort by')
- - branches_sort_options_hash.each do |value, title|
- %li
- = link_to title, filter_branches_path(sort: value), class: ("is-active" if @sort == value)
+ - unless @mode == 'overview'
+ .dropdown.inline>
+ %button.dropdown-menu-toggle{ type: 'button', 'data-toggle' => 'dropdown' }
+ %span.light
+ = branches_sort_options_hash[@sort]
+ = icon('chevron-down')
+ %ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-selectable
+ %li.dropdown-header
+ = s_('Branches|Sort by')
+ - branches_sort_options_hash.each do |value, title|
+ %li
+ = link_to title, project_branches_filtered_path(@project, state: 'all', search: params[:search], sort: value), class: ("is-active" if @sort == value)
- if can? current_user, :push_code, @project
= link_to project_merged_branches_path(@project),
@@ -35,7 +44,17 @@
= link_to new_project_branch_path(@project), class: 'btn btn-create' do
= s_('Branches|New branch')
- - if @branches.any?
+ - if can?(current_user, :admin_project, @project)
+ - project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
+ .row-content-block
+ %h5
+ = s_('Branches|Protected branches can be managed in %{project_settings_link}.').html_safe % { project_settings_link: project_settings_link }
+
+ - if @mode == 'overview' && (@active_branches.any? || @stale_branches.any?)
+ = render "projects/branches/panel", branches: @active_branches, state: 'active', panel_title: s_('Branches|Active branches'), show_more_text: s_('Branches|Show more active branches'), project: @project, overview_max_branches: @overview_max_branches
+ = render "projects/branches/panel", branches: @stale_branches, state: 'stale', panel_title: s_('Branches|Stale branches'), show_more_text: s_('Branches|Show more stale branches'), project: @project, overview_max_branches: @overview_max_branches
+
+ - elsif @branches.any?
%ul.content-list.all-branches
- @branches.each do |branch|
= render "projects/branches/branch", branch: branch, merged: @merged_branch_names.include?(branch.name)
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index e9d8fc75142..c7fc5a98ca8 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -28,4 +28,5 @@
.form-actions
= button_tag 'Create branch', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_branches_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 0cd2d45c74b..9126476e79e 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -63,7 +63,7 @@
- if admin
%td
- if job.project
- = link_to job.project.name_with_namespace, admin_project_path(job.project)
+ = link_to job.project.full_name, admin_project_path(job.project)
%td
- if job.try(:runner)
= runner_link(job.runner)
diff --git a/app/views/projects/clusters/show.html.haml b/app/views/projects/clusters/show.html.haml
index 2b1b23ba198..2ee0eafcf1a 100644
--- a/app/views/projects/clusters/show.html.haml
+++ b/app/views/projects/clusters/show.html.haml
@@ -10,11 +10,13 @@
install_helm_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :helm),
install_ingress_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :ingress),
install_prometheus_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :prometheus),
+ install_runner_path: install_applications_namespace_project_cluster_path(@cluster.project.namespace, @cluster.project, @cluster, :runner),
toggle_status: @cluster.enabled? ? 'true': 'false',
cluster_status: @cluster.status_name,
cluster_status_reason: @cluster.status_reason,
help_path: help_page_path('user/project/clusters/index.md', anchor: 'installing-applications'),
ingress_help_path: help_page_path('user/project/clusters/index.md', anchor: 'getting-the-external-ip-address'),
+ ingress_dns_help_path: help_page_path('topics/autodevops/quick_start_guide.md', anchor: 'point-dns-at-cluster-ip'),
manage_prometheus_path: edit_project_service_path(@cluster.project, 'prometheus') } }
.js-cluster-application-notice
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 3f699882c5f..68b35072f26 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -6,7 +6,3 @@
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
} }
-
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('commit_pipelines')
diff --git a/app/views/projects/commit/show.html.haml b/app/views/projects/commit/show.html.haml
index 4058e61eb9a..abb292f8f27 100644
--- a/app/views/projects/commit/show.html.haml
+++ b/app/views/projects/commit/show.html.haml
@@ -6,9 +6,6 @@
- @content_class = limited_container_width
- page_title "#{@commit.title} (#{@commit.short_id})", "Commits"
- page_description @commit.description
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('diff_notes')
.container-fluid{ class: [limited_container_width, container_class] }
= render "commit_box"
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 6ff7bcae54f..078bd0eee63 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -20,7 +20,7 @@
.avatar-cell.hidden-xs
= author_avatar(commit, size: 36)
- .commit-detail
+ .commit-detail.flex-list
.commit-content
= link_to_markdown_field(commit, :title, link, class: "commit-row-message item-title")
%span.commit-row-message.visible-xs-inline
diff --git a/app/views/projects/cycle_analytics/show.html.haml b/app/views/projects/cycle_analytics/show.html.haml
index d98e0564da4..02395b6eb9b 100644
--- a/app/views/projects/cycle_analytics/show.html.haml
+++ b/app/views/projects/cycle_analytics/show.html.haml
@@ -2,7 +2,6 @@
- page_title "Cycle Analytics"
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('cycle_analytics')
#cycle-analytics{ class: container_class, "v-cloak" => "true", data: { request_path: project_cycle_analytics_path(@project) } }
- if @cycle_analytics_no_data
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 0b01e38d23d..47bfcb21cf4 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -17,7 +17,7 @@
\
- if editable_diff?(diff_file)
- link_opts = @merge_request.persisted? ? { from_merge_request_iid: @merge_request.iid } : {}
- = edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
+ = edit_blob_button(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
blob: blob, link_opts: link_opts)
- if image_diff && image_replaced
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 0931ceb1512..a96485ab155 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -70,6 +70,7 @@
Enable or disable certain project features and choose access levels.
.settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
+ -# haml-lint:disable InlineJavaScript
%script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
.js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
@@ -85,7 +86,7 @@
.settings-content
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "merge-request-settings-form" }, authenticity_token: true do |f|
= render 'merge_request_settings', form: f
- = f.submit 'Save changes', class: "btn btn-save"
+ = f.submit 'Save changes', class: "btn btn-save qa-save-merge-request-changes"
= render 'export', project: @project
diff --git a/app/views/projects/environments/folder.html.haml b/app/views/projects/environments/folder.html.haml
index eca10d99908..1ac7dab6775 100644
--- a/app/views/projects/environments/folder.html.haml
+++ b/app/views/projects/environments/folder.html.haml
@@ -1,10 +1,6 @@
- @no_container = true
- page_title "Environments"
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag("environments_folder")
-
#environments-folder-list-view{ data: { endpoint: folder_project_environments_path(@project, @folder, format: :json),
"folder-name" => @folder,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index 31cf173fa9c..0d656b25bc8 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag("common_vue")
- = webpack_bundle_tag("environments")
#environments-list-view{ data: { environments_data: environments_list_data,
"can-create-deployment" => can?(current_user, :create_deployment, @project).to_s,
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 91b3743e9e7..9d9759ebc5f 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -1,7 +1,5 @@
- @no_container = true
- page_title "Metrics for environment", @environment.name
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag 'common_vue'
.prometheus-container{ class: container_class }
.top-area
diff --git a/app/views/projects/environments/terminal.html.haml b/app/views/projects/environments/terminal.html.haml
index 7be4ef39117..6ec4ff56552 100644
--- a/app/views/projects/environments/terminal.html.haml
+++ b/app/views/projects/environments/terminal.html.haml
@@ -3,7 +3,6 @@
- content_for :page_specific_javascripts do
= stylesheet_link_tag "xterm/xterm"
- = webpack_bundle_tag("terminal")
%div{ class: container_class }
.top-area
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
index 2599ce5c4b8..620fd1906ba 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml
@@ -53,7 +53,7 @@
- if admin
%td
- if generic_commit_status.project
- = link_to generic_commit_status.project.name_with_namespace, admin_project_path(generic_commit_status.project)
+ = link_to generic_commit_status.project.full_name, admin_project_path(generic_commit_status.project)
%td
- if generic_commit_status.try(:runner)
= runner_link(generic_commit_status.runner)
diff --git a/app/views/projects/graphs/charts.html.haml b/app/views/projects/graphs/charts.html.haml
index d4b4a6203f3..14c47a5d91c 100644
--- a/app/views/projects/graphs/charts.html.haml
+++ b/app/views/projects/graphs/charts.html.haml
@@ -74,6 +74,7 @@
= _("Commits per day hour (UTC)")
%canvas#hour-chart
+-# haml-lint:disable InlineJavaScript
%script#projectChartData{ type: "application/json" }
- projectChartData = {};
- projectChartData['hour'] = @commits_per_time
diff --git a/app/views/projects/imports/show.html.haml b/app/views/projects/imports/show.html.haml
index 8c490773a56..3b0c828ccd1 100644
--- a/app/views/projects/imports/show.html.haml
+++ b/app/views/projects/imports/show.html.haml
@@ -1,12 +1,11 @@
-- page_title @project.forked? ? "Forking in progress" : "Import in progress"
+- page_title import_in_progress_title
+
.save-project-loader
.center
%h2
%i.fa.fa-spinner.fa-spin
- - if @project.forked?
- Forking in progress.
- - else
- Import in progress.
- - if @project.external_import?
+ = import_in_progress_title
+ - if !has_ci_cd_only_params? && @project.external_import?
%p.monospace git clone --bare #{@project.safe_import_url}
- %p Please wait while we import the repository for you. Refresh at will.
+ %p
+ = import_wait_and_refresh_message
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index 11b5e02f1e0..cdfc3e232c5 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -6,14 +6,6 @@
= link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, format: 'json'), data: {original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
%section.js-vue-notes-event
- #js-vue-notes{ data: { discussions_path: discussions_project_issue_path(@project, @issue, format: :json),
- register_path: new_session_path(:user, redirect_to_referer: 'yes', anchor: 'register-pane'),
- new_session_path: new_session_path(:user, redirect_to_referer: 'yes'),
- markdown_docs_path: help_page_path('user/markdown'),
- quick_actions_docs_path: help_page_path('user/project/quick_actions'),
- notes_path: notes_url,
- close_issue_path: issue_path(@issue, issue: { state_event: :close }, format: 'json'),
- reopen_issue_path: issue_path(@issue, issue: { state_event: :reopen }, format: 'json'),
- last_fetched_at: Time.now.to_i,
- noteable_data: serialize_issuable(@issue),
- current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
+ #js-vue-notes{ data: { notes_data: notes_data(@issue),
+ noteable_data: serialize_issuable(@issue),
+ current_user_data: UserSerializer.new.represent(current_user, only_path: true).to_json } }
diff --git a/app/views/projects/issues/_merge_requests.html.haml b/app/views/projects/issues/_merge_requests.html.haml
index 5f97d31f610..5c36d2202a6 100644
--- a/app/views/projects/issues/_merge_requests.html.haml
+++ b/app/views/projects/issues/_merge_requests.html.haml
@@ -18,7 +18,7 @@
- unless @issue.project.id == merge_request.target_project.id
in
- project = merge_request.target_project
- = link_to project.name_with_namespace, project_path(project)
+ = link_to project.full_name, project_path(project)
- if merge_request.merged?
%span.merge-request-status.prepend-left-10.merged
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 91f68d8c419..ec7e87219f5 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -55,7 +55,8 @@
.issue-details.issuable-details
.detail-page-description.content-block
- %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue)
+ -# haml-lint:disable InlineJavaScript
+ %script#js-issuable-app-initial-data{ type: "application/json" }= issuable_initial_data(@issue).to_json
#js-issuable-app
%h2.title= markdown_field(@issue, :title)
- if @issue.description.present?
@@ -73,7 +74,7 @@
.content-block.emoji-block
.row
- .col-sm-8.js-issue-note-awards
+ .col-sm-8.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @issue, inline: true
.col-sm-4.new-branch-col
= render 'new_branch' unless @issue.confidential?
@@ -82,6 +83,3 @@
= render 'projects/issues/discussion'
= render 'shared/issuable/sidebar', issuable: @issue
-
-= webpack_bundle_tag('common_vue')
-= webpack_bundle_tag('issue_show')
diff --git a/app/views/projects/merge_requests/conflicts.html.haml b/app/views/projects/merge_requests/conflicts.html.haml
index 2a2e57027be..a6e2565a485 100644
--- a/app/views/projects/merge_requests/conflicts.html.haml
+++ b/app/views/projects/merge_requests/conflicts.html.haml
@@ -1,7 +1,5 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/mr_title"
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index 2a2e57027be..a6e2565a485 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -1,7 +1,5 @@
- page_title "Merge Conflicts", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('merge_conflicts')
= page_specific_javascript_tag('lib/ace.js')
= render "projects/merge_requests/mr_title"
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index e29f21b3bec..9866cc716ee 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -1,11 +1,10 @@
+- @gfm_form = true
- @content_class = "limit-container-width" unless fluid_layout
- add_to_breadcrumbs "Merge Requests", project_merge_requests_path(@project)
- breadcrumb_title @merge_request.to_reference
- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
.merge-request{ data: { mr_action: j(params[:tab].presence || 'show'), url: merge_request_path(@merge_request, format: :json), project_path: project_path(@merge_request.project) } }
= render "projects/merge_requests/mr_title"
@@ -23,10 +22,7 @@
#js-vue-mr-widget.mr-widget
- - content_for :page_specific_javascripts do
- = webpack_bundle_tag 'vue_merge_request_widget'
-
- .content-block.content-block-small.emoji-list-container
+ .content-block.content-block-small.emoji-list-container.js-noteable-awards
= render 'award_emoji/awards_block', awardable: @merge_request, inline: true
.merge-request-tabs-holder{ class: ("js-tabs-affix" unless ENV['RAILS_ENV'] == 'test') }
@@ -54,28 +50,37 @@
= tab_link_for @merge_request, :diffs do
Changes
%span.badge= @merge_request.diff_size
- #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
- %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
- %div
- .line-resolve-all{ "v-show" => "discussionCount > 0",
- ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
- %span.line-resolve-btn.is-disabled{ type: "button",
- ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
- %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
- = render 'shared/icons/icon_status_success_solid.svg'
- %template{ 'v-else' => '' }
- = render 'shared/icons/icon_resolve_discussion.svg'
- %span.line-resolve-text
- {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
- = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
- = render "discussions/jump_to_next"
+
+ - if has_vue_discussions_cookie?
+ #js-vue-discussion-counter
+ - else
+ #resolve-count-app.line-resolve-all-container.prepend-top-10{ "v-cloak" => true }
+ %resolve-count{ "inline-template" => true, ":logged-out" => "#{current_user.nil?}" }
+ %div
+ .line-resolve-all{ "v-show" => "discussionCount > 0",
+ ":class" => "{ 'has-next-btn': !loggedOut && resolvedDiscussionCount !== discussionCount }" }
+ %span.line-resolve-btn.is-disabled{ type: "button",
+ ":class" => "{ 'is-active': resolvedDiscussionCount === discussionCount }" }
+ %template{ 'v-if' => 'resolvedDiscussionCount === discussionCount' }
+ = render 'shared/icons/icon_status_success_solid.svg'
+ %template{ 'v-else' => '' }
+ = render 'shared/icons/icon_resolve_discussion.svg'
+ %span.line-resolve-text
+ {{ resolvedDiscussionCount }}/{{ discussionCount }} {{ resolvedCountText }} resolved
+ = render "discussions/new_issue_for_all_discussions", merge_request: @merge_request
+ = render "discussions/jump_to_next"
.tab-content#diff-notes-app
#notes.notes.tab-pane.voting_notes
.row
%section.col-md-12
- .issuable-discussion
+ %script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
+ .issuable-discussion.js-vue-notes-event
= render "projects/merge_requests/discussion"
+ - if has_vue_discussions_cookie?
+ #js-vue-mr-discussions{ data: { notes_data: notes_data(@merge_request),
+ noteable_data: serialize_issuable(@merge_request),
+ current_user_data: UserSerializer.new.represent(current_user).to_json} }
#commits.commits.tab-pane
-# This tab is always loaded via AJAX
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index 97be8950db0..4b7be9a223f 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -1,7 +1,5 @@
- breadcrumb_title "Graph"
- page_title "Graph", @ref
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('network')
= render "head"
%div{ class: container_class }
.project-network
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index 679ba23a4db..1d31b58a2cc 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -12,11 +12,14 @@
.row.prepend-top-default
.col-lg-3.profile-settings-sidebar
%h4.prepend-top-0
- New project
+ = _('New project')
%p
- A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), #{link_to 'among other things', help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'}.
+ - among_other_things_link = link_to _('among other things'), help_page_path("user/project/index.md", anchor: "projects-features"), target: '_blank'
+ = _('A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}.').html_safe % { among_other_things_link: among_other_things_link }
%p
- All features are enabled when you create a project, but you can disable the ones you don’t need in the project settings.
+ = _('All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings.')
+ -# EE-specific start
+ -# EE-specific end
.md
= brand_new_project_guidelines
%p
@@ -28,36 +31,38 @@
.col-lg-9.js-toggle-container
%ul.nav-links.gitlab-tabs{ role: 'tablist' }
- %li{ class: ('active' if active_tab == 'blank'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'blank'), role: 'presentation' }
%a{ href: '#blank-project-pane', id: 'blank-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Blank project
%span.visible-xs Blank
- %li{ class: ('active' if active_tab == 'template'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'template'), role: 'presentation' }
%a{ href: '#create-from-template-pane', id: 'create-from-template-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Create from template
%span.visible-xs Template
- %li{ class: ('active' if active_tab == 'import'), role: 'presentation' }
+ %li{ class: active_when(active_tab == 'import'), role: 'presentation' }
%a{ href: '#import-project-pane', id: 'import-project-tab', data: { toggle: 'tab' }, role: 'tab' }
%span.hidden-xs Import project
%span.visible-xs Import
+ -# EE-specific start
+ -# EE-specific end
.tab-content.gitlab-tab-content
- .tab-pane{ id: 'blank-project-pane', class: ('active' if active_tab == 'blank'), role: 'tabpanel' }
+ .tab-pane{ id: 'blank-project-pane', class: active_when(active_tab == 'blank'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
= render 'new_project_fields', f: f, project_name_id: "blank-project-name"
- .tab-pane.no-padding{ id: 'create-from-template-pane', class: ('active' if active_tab == 'template'), role: 'tabpanel' }
+ .tab-pane.no-padding{ id: 'create-from-template-pane', class: active_when(active_tab == 'template'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
.project-template
.form-group
%div
= render 'project_templates', f: f
- .tab-pane.import-project-pane{ id: 'import-project-pane', class: ('active' if active_tab == 'import'), role: 'tabpanel' }
+ .tab-pane.import-project-pane.js-toggle-container{ id: 'import-project-pane', class: active_when(active_tab == 'import'), role: 'tabpanel' }
= form_for @project, html: { class: 'new_project' } do |f|
- if import_sources_enabled?
.project-import.row
- .col-sm-12
+ .col-lg-12
.form-group.import-btn-container.clearfix
= f.label :visibility_level, class: 'label-light' do #the label here seems wrong
Import project from
@@ -97,7 +102,7 @@
Gitea
%div
- if git_import_enabled?
- %button.btn.js-toggle-button.import_git{ type: "button" }
+ %button.btn.js-toggle-button.js-import-git-toggle-button{ type: "button", data: { toggle_open_class: 'active' } }
= icon('git', text: 'Repo by URL')
.col-lg-12
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
@@ -105,6 +110,10 @@
= render "shared/import_form", f: f
= render 'new_project_fields', f: f, project_name_id: "import-url-name"
+
+ -# EE-specific start
+ -# EE-specific end
+
.save-project-loader.hide
.center
%h2
diff --git a/app/views/projects/pages_domains/_form.html.haml b/app/views/projects/pages_domains/_form.html.haml
index ca1b41b140a..d81b07832bb 100644
--- a/app/views/projects/pages_domains/_form.html.haml
+++ b/app/views/projects/pages_domains/_form.html.haml
@@ -1,34 +1,30 @@
-= form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
- - if @domain.errors.any?
- #error_explanation
- .alert.alert-danger
- - @domain.errors.full_messages.each do |msg|
- %p= msg
+- if @domain.errors.any?
+ #error_explanation
+ .alert.alert-danger
+ - @domain.errors.full_messages.each do |msg|
+ %p= msg
+.form-group
+ = f.label :domain, class: 'control-label' do
+ Domain
+ .col-sm-10
+ = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control', disabled: @domain.persisted?
+
+- if Gitlab.config.pages.external_https
.form-group
- = f.label :domain, class: 'control-label' do
- Domain
+ = f.label :certificate, class: 'control-label' do
+ Certificate (PEM)
.col-sm-10
- = f.text_field :domain, required: true, autocomplete: 'off', class: 'form-control'
-
- - if Gitlab.config.pages.external_https
- .form-group
- = f.label :certificate, class: 'control-label' do
- Certificate (PEM)
- .col-sm-10
- = f.text_area :certificate, rows: 5, class: 'form-control'
- %span.help-inline Upload a certificate for your domain with all intermediates
-
- .form-group
- = f.label :key, class: 'control-label' do
- Key (PEM)
- .col-sm-10
- = f.text_area :key, rows: 5, class: 'form-control'
- %span.help-inline Upload a private key for your certificate
- - else
- .nothing-here-block
- Support for custom certificates is disabled.
- Ask your system's administrator to enable it.
+ = f.text_area :certificate, rows: 5, class: 'form-control'
+ %span.help-inline Upload a certificate for your domain with all intermediates
- .form-actions
- = f.submit 'Create New Domain', class: "btn btn-save"
+ .form-group
+ = f.label :key, class: 'control-label' do
+ Key (PEM)
+ .col-sm-10
+ = f.text_area :key, rows: 5, class: 'form-control'
+ %span.help-inline Upload a private key for your certificate
+- else
+ .nothing-here-block
+ Support for custom certificates is disabled.
+ Ask your system's administrator to enable it.
diff --git a/app/views/projects/pages_domains/edit.html.haml b/app/views/projects/pages_domains/edit.html.haml
new file mode 100644
index 00000000000..5645a4604bf
--- /dev/null
+++ b/app/views/projects/pages_domains/edit.html.haml
@@ -0,0 +1,11 @@
+- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- breadcrumb_title @domain.domain
+- page_title @domain.domain
+%h3.page_title
+ = @domain.domain
+%hr.clearfix
+%div
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = render 'form', { f: f }
+ .form-actions
+ = f.submit 'Save Changes', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/new.html.haml b/app/views/projects/pages_domains/new.html.haml
index e1477c71d06..5a397c9d3c7 100644
--- a/app/views/projects/pages_domains/new.html.haml
+++ b/app/views/projects/pages_domains/new.html.haml
@@ -1,6 +1,10 @@
+- add_to_breadcrumbs "Pages", project_pages_path(@project)
- page_title 'New Pages Domain'
%h3.page_title
New Pages Domain
%hr.clearfix
%div
- = render 'form'
+ = form_for [@project.namespace.becomes(Namespace), @project, @domain], html: { class: 'form-horizontal fieldset-form' } do |f|
+ = render 'form', { f: f }
+ .form-actions
+ = f.submit 'Create New Domain', class: "btn btn-save"
diff --git a/app/views/projects/pages_domains/show.html.haml b/app/views/projects/pages_domains/show.html.haml
index 72e9203bdb0..ba0713daee9 100644
--- a/app/views/projects/pages_domains/show.html.haml
+++ b/app/views/projects/pages_domains/show.html.haml
@@ -1,4 +1,7 @@
+- add_to_breadcrumbs "Pages", project_pages_path(@project)
+- breadcrumb_title @domain.domain
- page_title "#{@domain.domain}", 'Pages Domains'
+
- verification_enabled = Gitlab::CurrentSettings.pages_domain_verification_enabled?
- if verification_enabled && @domain.unverified?
%p.alert.alert-warning
@@ -8,6 +11,7 @@
%h3.page-title
Pages Domain
+ = link_to 'Edit', edit_project_pages_domain_path(@project, @domain), class: 'btn btn-success pull-right'
.table-holder
%table.table
diff --git a/app/views/projects/pipelines/charts/_pipeline_times.haml b/app/views/projects/pipelines/charts/_pipeline_times.haml
index 510697c2ae9..c23fe6ff170 100644
--- a/app/views/projects/pipelines/charts/_pipeline_times.haml
+++ b/app/views/projects/pipelines/charts/_pipeline_times.haml
@@ -4,4 +4,5 @@
%canvas#build_timesChart{ height: 200 }
+-# haml-lint:disable InlineJavaScript
%script#pipelinesTimesChartsData{ type: "application/json" }= { :labels => @charts[:pipeline_times].labels, :values => @charts[:pipeline_times].pipeline_times }.to_json.html_safe
diff --git a/app/views/projects/pipelines/charts/_pipelines.haml b/app/views/projects/pipelines/charts/_pipelines.haml
index 2f4b6def155..14b3d47a9c2 100644
--- a/app/views/projects/pipelines/charts/_pipelines.haml
+++ b/app/views/projects/pipelines/charts/_pipelines.haml
@@ -26,6 +26,7 @@
= _("Pipelines for last year")
%canvas#yearChart.padded{ height: 250 }
+-# haml-lint:disable InlineJavaScript
%script#pipelinesChartsData{ type: "application/json" }
- chartData = []
- [:week, :month, :year].each do |scope|
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index fdcc60f48a5..3e6b3346787 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -7,11 +7,9 @@
"help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
"empty-state-svg-path" => image_path('illustrations/pipelines_empty.svg'),
"error-state-svg-path" => image_path('illustrations/pipelines_failed.svg'),
- "new-pipeline-path" => new_project_pipeline_path(@project),
+ "no-pipelines-svg-path" => image_path('illustrations/pipelines_pending.svg'),
"can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
- "has-ci" => @repository.gitlab_ci_yml,
- "ci-lint-path" => ci_lint_path,
- "reset-cache-path" => reset_cache_project_settings_ci_cd_path(@project) } }
-
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pipelines')
+ "new-pipeline-path" => can?(current_user, :create_pipeline, @project) && new_project_pipeline_path(@project),
+ "ci-lint-path" => can?(current_user, :create_pipeline, @project) && ci_lint_path,
+ "reset-cache-path" => can?(current_user, :admin_pipeline, @project) && reset_cache_project_settings_ci_cd_path(@project) ,
+ "has-gitlab-ci" => (@project.has_ci? && @project.builds_enabled?).to_s } }
diff --git a/app/views/projects/pipelines/new.html.haml b/app/views/projects/pipelines/new.html.haml
index 4ad37d0e882..877101b05ca 100644
--- a/app/views/projects/pipelines/new.html.haml
+++ b/app/views/projects/pipelines/new.html.haml
@@ -20,4 +20,5 @@
= f.submit 'Create pipeline', class: 'btn btn-create', tabindex: 3
= link_to 'Cancel', project_pipelines_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/pipelines/show.html.haml b/app/views/projects/pipelines/show.html.haml
index 2174154b207..ffb0ae95f9b 100644
--- a/app/views/projects/pipelines/show.html.haml
+++ b/app/views/projects/pipelines/show.html.haml
@@ -13,4 +13,3 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('pipelines_details')
diff --git a/app/views/projects/project_members/update.js.haml b/app/views/projects/project_members/update.js.haml
deleted file mode 100644
index d15f4310ff5..00000000000
--- a/app/views/projects/project_members/update.js.haml
+++ /dev/null
@@ -1,4 +0,0 @@
-:plain
- var $listItem = $('#{escape_javascript(render('shared/members/member', member: @project_member))}');
- $("##{dom_id(@project_member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
- gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(@project_member)}"));
diff --git a/app/views/projects/protected_branches/_index.html.haml b/app/views/projects/protected_branches/_index.html.haml
index 127a338e413..2b0a502fe4d 100644
--- a/app/views/projects/protected_branches/_index.html.haml
+++ b/app/views/projects/protected_branches/_index.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('protected_branches')
-
- content_for :create_protected_branch do
= render 'projects/protected_branches/create_protected_branch'
diff --git a/app/views/projects/protected_tags/_index.html.haml b/app/views/projects/protected_tags/_index.html.haml
index 74f7f63c941..6b284fda35c 100644
--- a/app/views/projects/protected_tags/_index.html.haml
+++ b/app/views/projects/protected_tags/_index.html.haml
@@ -1,6 +1,3 @@
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('protected_tags')
-
- content_for :create_protected_tag do
= render 'projects/protected_tags/create_protected_tag'
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index 744b88760bc..27e1f9fba3e 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -15,7 +15,6 @@
#js-vue-registry-images{ data: { endpoint: project_container_registry_index_path(@project, format: :json) } }
= webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('registry_list')
.row.prepend-top-10
.col-lg-12
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index e660fce652f..49c90869146 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -30,6 +30,11 @@
.col-sm-10
= f.text_field :token, class: 'form-control', readonly: true
.form-group
+ = label_tag :ip_address, class: 'control-label' do
+ IP Address
+ .col-sm-10
+ = f.text_field :ip_address, class: 'form-control', readonly: true
+ .form-group
= label_tag :description, class: 'control-label' do
Description
.col-sm-10
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index dfab04aa1fb..4e57f5f844d 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -41,6 +41,9 @@
%td Version
%td= @runner.version
%tr
+ %td IP Address
+ %td= @runner.ip_address
+ %tr
%td Revision
%td= @runner.revision
%tr
diff --git a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
index 5dbcbf7eba6..2ab0227126a 100644
--- a/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
+++ b/app/views/projects/services/mattermost_slash_commands/_detailed_help.html.haml
@@ -1,4 +1,4 @@
-- run_actions_text = "Perform common operations on GitLab project: #{@project.name_with_namespace}"
+- run_actions_text = "Perform common operations on GitLab project: #{@project.full_name}"
%p To setup this service:
%ul.list-unstyled.indent-list
@@ -20,7 +20,7 @@
.form-group
= label_tag :display_name, 'Display name', class: 'col-sm-2 col-xs-12 control-label'
.col-sm-10.col-xs-12.input-group
- = text_field_tag :display_name, "GitLab / #{@project.name_with_namespace}", class: 'form-control input-sm', readonly: 'readonly'
+ = text_field_tag :display_name, "GitLab / #{@project.full_name}", class: 'form-control input-sm', readonly: 'readonly'
.input-group-btn
= clipboard_button(target: '#display_name')
diff --git a/app/views/projects/services/prometheus/_configuration_banner.html.haml b/app/views/projects/services/prometheus/_configuration_banner.html.haml
new file mode 100644
index 00000000000..2cc2a6b2b5b
--- /dev/null
+++ b/app/views/projects/services/prometheus/_configuration_banner.html.haml
@@ -0,0 +1,26 @@
+%h4
+ = s_('PrometheusService|Auto configuration')
+
+- if service.manual_configuration?
+ .well
+ = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
+- else
+ .container-fluid
+ .row
+ - if service.prometheus_installed?
+ .col-sm-2
+ .svg-container
+ = image_tag 'illustrations/monitoring/getting_started.svg'
+ .col-sm-10
+ %p.text-success.prepend-top-default
+ = s_('PrometheusService|Prometheus is being automatically managed on your clusters')
+ = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(project), class: 'btn'
+ - else
+ .col-sm-2
+ = image_tag 'illustrations/monitoring/loading.svg'
+ .col-sm-10
+ %p.prepend-top-default
+ = s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
+ = link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(project), class: 'btn btn-success'
+
+%hr
diff --git a/app/views/projects/services/prometheus/_help.html.haml b/app/views/projects/services/prometheus/_help.html.haml
index 5e320a252d8..88acb824ba7 100644
--- a/app/views/projects/services/prometheus/_help.html.haml
+++ b/app/views/projects/services/prometheus/_help.html.haml
@@ -1,29 +1,5 @@
-%h4
- = s_('PrometheusService|Auto configuration')
-
-- if @service.manual_configuration?
- .well
- = s_('PrometheusService|To enable the installation of Prometheus on your clusters, deactivate the manual configuration below')
-- else
- .container-fluid
- .row
- - if @service.prometheus_installed?
- .col-sm-2
- .svg-container
- = image_tag 'illustrations/monitoring/getting_started.svg'
- .col-sm-10
- %p.text-success.prepend-top-default
- = s_('PrometheusService|Prometheus is being automatically managed on your clusters')
- = link_to s_('PrometheusService|Manage clusters'), project_clusters_path(@project), class: 'btn'
- - else
- .col-sm-2
- = image_tag 'illustrations/monitoring/loading.svg'
- .col-sm-10
- %p.prepend-top-default
- = s_('PrometheusService|Automatically deploy and configure Prometheus on your clusters to monitor your project’s environments')
- = link_to s_('PrometheusService|Install Prometheus on clusters'), project_clusters_path(@project), class: 'btn btn-success'
-
-%hr
+- if @project
+ = render 'projects/services/prometheus/configuration_banner', project: @project, service: @service
%h4.append-bottom-default
= s_('PrometheusService|Manual configuration')
diff --git a/app/views/projects/services/slack_slash_commands/_help.html.haml b/app/views/projects/services/slack_slash_commands/_help.html.haml
index c31c95608c6..d592a5e4663 100644
--- a/app/views/projects/services/slack_slash_commands/_help.html.haml
+++ b/app/views/projects/services/slack_slash_commands/_help.html.haml
@@ -1,4 +1,4 @@
-- pretty_name = defined?(@project) ? @project.name_with_namespace : 'namespace / path'
+- pretty_name = defined?(@project) ? @project.full_name : 'namespace / path'
- run_actions_text = "Perform common operations on GitLab project: #{pretty_name}"
.well
diff --git a/app/views/projects/settings/repository/show.html.haml b/app/views/projects/settings/repository/show.html.haml
index 3077203c2a6..235d532bf98 100644
--- a/app/views/projects/settings/repository/show.html.haml
+++ b/app/views/projects/settings/repository/show.html.haml
@@ -4,7 +4,6 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('deploy_keys')
-# Protected branches & tags use a lot of nested partials.
-# The shared parts of the views can be found in the `shared` directory.
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index 6e105a5521a..1827a3d323c 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -43,4 +43,5 @@
.form-actions
= button_tag s_('TagsPage|Create tag'), class: 'btn btn-create'
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'btn btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tree/_tree_header.html.haml b/app/views/projects/tree/_tree_header.html.haml
index 05539dfed7c..06bce52e709 100644
--- a/app/views/projects/tree/_tree_header.html.haml
+++ b/app/views/projects/tree/_tree_header.html.haml
@@ -72,11 +72,6 @@
#{ _('New tag') }
.tree-controls
- - 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/search/_category.html.haml b/app/views/search/_category.html.haml
index 915e648a5d3..7d43fd61081 100644
--- a/app/views/search/_category.html.haml
+++ b/app/views/search/_category.html.haml
@@ -14,25 +14,25 @@
= link_to search_filter_path(scope: 'issues') do
Issues
%span.badge
- = @search_results.issues_count
+ = limited_count(@search_results.limited_issues_count)
- if project_search_tabs?(:merge_requests)
%li{ class: active_when(@scope == 'merge_requests') }
= link_to search_filter_path(scope: 'merge_requests') do
Merge requests
%span.badge
- = @search_results.merge_requests_count
+ = limited_count(@search_results.limited_merge_requests_count)
- if project_search_tabs?(:milestones)
%li{ class: active_when(@scope == 'milestones') }
= link_to search_filter_path(scope: 'milestones') do
Milestones
%span.badge
- = @search_results.milestones_count
+ = limited_count(@search_results.limited_milestones_count)
- if project_search_tabs?(:notes)
%li{ class: active_when(@scope == 'notes') }
= link_to search_filter_path(scope: 'notes') do
Comments
%span.badge
- = @search_results.notes_count
+ = limited_count(@search_results.limited_notes_count)
- if project_search_tabs?(:wiki)
%li{ class: active_when(@scope == 'wiki_blobs') }
= link_to search_filter_path(scope: 'wiki_blobs') do
diff --git a/app/views/search/_filter.html.haml b/app/views/search/_filter.html.haml
index e43796e9654..e4902d368e7 100644
--- a/app/views/search/_filter.html.haml
+++ b/app/views/search/_filter.html.haml
@@ -22,7 +22,7 @@
%span.dropdown-toggle-text
Project:
- if @project.present?
- = @project.name_with_namespace
+ = @project.full_name
- else
Any
= icon("chevron-down")
diff --git a/app/views/search/_results.html.haml b/app/views/search/_results.html.haml
index 60ef44482f0..ab56f48ba4d 100644
--- a/app/views/search/_results.html.haml
+++ b/app/views/search/_results.html.haml
@@ -6,7 +6,7 @@
= search_entries_info(@search_objects, @scope, @search_term)
- unless @show_snippets
- if @project
- in project #{link_to @project.name_with_namespace, [@project.namespace.becomes(Namespace), @project]}
+ in project #{link_to @project.full_name, [@project.namespace.becomes(Namespace), @project]}
- elsif @group
in group #{link_to @group.name, @group}
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index b4bc8982c05..b7a27ef6be2 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -10,4 +10,4 @@
.description.term
= search_md_sanitize(issue, :description)
%span.light
- #{issue.project.name_with_namespace}
+ #{issue.project.full_name}
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 1a5499e4d58..8b0fd74f680 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -11,4 +11,4 @@
.description.term
= search_md_sanitize(merge_request, :description)
%span.light
- #{merge_request.project.name_with_namespace}
+ #{merge_request.project.full_name}
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index a7e178dfa71..e4ab7b0541f 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -7,7 +7,7 @@
%i.fa.fa-comment
= link_to_member(project, note.author, avatar: false)
commented on
- = link_to project.name_with_namespace, project
+ = link_to project.full_name, project
&middot;
- if note.for_commit?
diff --git a/app/views/search/results/_snippet_title.html.haml b/app/views/search/results/_snippet_title.html.haml
index 65710c09a89..d46c4d11e51 100644
--- a/app/views/search/results/_snippet_title.html.haml
+++ b/app/views/search/results/_snippet_title.html.haml
@@ -11,7 +11,7 @@
%small.pull-right.cgray
- if snippet_title.project_id?
- = link_to snippet_title.project.name_with_namespace, project_path(snippet_title.project)
+ = link_to snippet_title.project.full_name, project_path(snippet_title.project)
.snippet-info
= snippet_title.to_reference
diff --git a/app/views/sent_notifications/unsubscribe.html.haml b/app/views/sent_notifications/unsubscribe.html.haml
index de52fd00157..7d3e243495f 100644
--- a/app/views/sent_notifications/unsubscribe.html.haml
+++ b/app/views/sent_notifications/unsubscribe.html.haml
@@ -1,7 +1,7 @@
- noteable = @sent_notification.noteable
- noteable_type = @sent_notification.noteable_type.titleize.downcase
- noteable_text = %(#{noteable.title} (#{noteable.to_reference}))
-- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.name_with_namespace
+- page_title "Unsubscribe", noteable_text, noteable_type.pluralize, @sent_notification.project.full_name
%h3.page-title
Unsubscribe from #{noteable_type}
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index 736afa085e8..5eaaa1448d5 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -1,17 +1,22 @@
+- ci_cd_only = local_assigns.fetch(:ci_cd_only, false)
+
.form-group.import-url-data
= f.label :import_url, class: 'label-light' do
- %span Git repository URL
+ %span
+ = _('Git repository URL')
- = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git'
+ = f.text_field :import_url, autocomplete: 'off', class: 'form-control', placeholder: 'https://username:password@gitlab.company.com/group/project.git', required: true
.well.prepend-top-20
%ul
%li
- The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.
+ = _('The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>.').html_safe
%li
- If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.
+ = _('If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>.').html_safe
%li
- The import will time out after #{time_interval_in_words(Gitlab.config.gitlab_shell.git_timeout)}.
- For repositories that take longer, use a clone/push combination.
+ = import_will_timeout_message(ci_cd_only)
%li
- To migrate an SVN repository, check out #{link_to "this document", help_page_path('user/project/import/svn')}.
+ = import_svn_message(ci_cd_only)
+
+-# EE-specific start
+-# EE-specific end
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index 479bd2cdb38..4c8c92d722a 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,6 +1,5 @@
- show_create = local_assigns.fetch(:show_create, false)
-- 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
@@ -16,14 +15,3 @@
= dropdown_filter _("Search branches and tags")
= dropdown_content
= dropdown_loading
- - if show_new_branch_form
- = dropdown_footer do
- %ul.dropdown-footer-list
- %li
- %a.dropdown-toggle-page{ href: "#" }
- Create new branch
- - if show_new_branch_form
- .dropdown-page-two
- = dropdown_title("Create new branch", options: { back: true })
- = dropdown_content do
- .js-new-branch-dropdown
diff --git a/app/views/shared/boards/_show.html.haml b/app/views/shared/boards/_show.html.haml
index a10fc42b82d..014b8de1dc9 100644
--- a/app/views/shared/boards/_show.html.haml
+++ b/app/views/shared/boards/_show.html.haml
@@ -6,8 +6,8 @@
- content_for :page_specific_javascripts do
= webpack_bundle_tag 'common_vue'
- = webpack_bundle_tag 'boards'
+ -# haml-lint:disable InlineJavaScript
%script#js-board-template{ type: "text/x-template" }= render "shared/boards/components/board"
%script#js-board-modal-filter{ type: "text/x-template" }= render "shared/issuable/search_bar", type: :boards_modal
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index dc583d3eb3b..adaddda13eb 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,7 +1,4 @@
- todo = issuable_todo(issuable)
-- content_for :page_specific_javascripts do
- = webpack_bundle_tag('common_vue')
- = webpack_bundle_tag('sidebar')
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: current_user.present? } }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar{ data: { endpoint: "#{issuable_json_path(issuable)}" } }
@@ -120,10 +117,12 @@
= render partial: "shared/issuable/label_page_create"
- if issuable.has_attribute?(:confidential)
+ -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: @issue.confidential, is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
- if issuable.has_attribute?(:discussion_locked)
+ -# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: issuable.discussion_locked?, is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
@@ -160,4 +159,5 @@
= _('Move')
= icon('spinner spin', class: 'sidebar-move-issue-confirmation-loading-icon')
+ -# haml-lint:disable InlineJavaScript
%script.js-sidebar-options{ type: "application/json" }= issuable_sidebar_options(issuable, can_edit_issuable).to_json.html_safe
diff --git a/app/views/shared/members/update.js.haml b/app/views/shared/members/update.js.haml
new file mode 100644
index 00000000000..55050bd8a15
--- /dev/null
+++ b/app/views/shared/members/update.js.haml
@@ -0,0 +1,6 @@
+- member = local_assigns.fetch(:member)
+
+:plain
+ var $listItem = $('#{escape_javascript(render('shared/members/member', member: member))}');
+ $("##{dom_id(member)} .list-item-name").replaceWith($listItem.find('.list-item-name'));
+ gl.utils.localTimeAgo($('.js-timeago'), $("##{dom_id(member)}"));
diff --git a/app/views/shared/milestones/_issuable.html.haml b/app/views/shared/milestones/_issuable.html.haml
index 129f6ab604e..eba64daaadc 100644
--- a/app/views/shared/milestones/_issuable.html.haml
+++ b/app/views/shared/milestones/_issuable.html.haml
@@ -12,7 +12,7 @@
- if show_project_name
%strong #{project.name} &middot;
- elsif show_full_project_name
- %strong #{project.name_with_namespace} &middot;
+ %strong #{project.full_name} &middot;
- if issuable.is_a?(Issue)
= confidential_icon(issuable)
= link_to issuable.title, issuable_url_args, title: issuable.title
diff --git a/app/views/shared/milestones/_milestone.html.haml b/app/views/shared/milestones/_milestone.html.haml
index e3b2b53833e..da01fc02d07 100644
--- a/app/views/shared/milestones/_milestone.html.haml
+++ b/app/views/shared/milestones/_milestone.html.haml
@@ -27,7 +27,7 @@
- milestone.milestones.each do |milestone|
= link_to milestone_path(milestone) do
%span.label.label-gray
- = dashboard ? milestone.project.name_with_namespace : milestone.project.name
+ = dashboard ? milestone.project.full_name : milestone.project.name
- if @group
.col-sm-6.milestone-actions
- if can?(current_user, :admin_milestones, @group)
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index cd4188daf5b..a942ebc328b 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -1,7 +1,5 @@
- affix_offset = local_assigns.fetch(:affix_offset, "50")
- project = local_assigns[:project]
-- content_for :page_specific_javascripts do
- = page_specific_javascript_bundle_tag('common_vue')
%aside.right-sidebar.js-right-sidebar{ data: { "offset-top" => affix_offset, "spy" => "affix", "always-show-toggle" => true }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite' }
.issuable-sidebar.milestone-sidebar
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index fd0760d83a5..6006ab8b43f 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -56,7 +56,7 @@
- milestone.milestones.each do |ms|
%tr
%td
- - project_name = group ? ms.project.name : ms.project.name_with_namespace
+ - project_name = group ? ms.project.name : ms.project.full_name
= link_to project_name, project_milestone_path(ms.project, ms)
%td
= ms.issues_visible_to_user(current_user).opened.count
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index b3f865c5b47..1db7c4e67cf 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -1,13 +1,14 @@
- issuable = @issue || @merge_request
- discussion_locked = issuable&.discussion_locked?
-%ul#notes-list.notes.main-notes-list.timeline
- = render "shared/notes/notes"
+- unless has_vue_discussions_cookie?
+ %ul#notes-list.notes.main-notes-list.timeline
+ = render "shared/notes/notes"
= render 'shared/notes/edit_form', project: @project
- if can_create_note?
- %ul.notes.notes-form.timeline
+ %ul.notes.notes-form.timeline{ :class => ('hidden' if has_vue_discussions_cookie?) }
%li.timeline-entry
.timeline-entry-inner
.flash-container.timeline-content
@@ -34,4 +35,5 @@
is locked. Only
%b project members
can comment.
+-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/snippets/_form.html.haml b/app/views/shared/snippets/_form.html.haml
index 2726a4934fb..c75c882a693 100644
--- a/app/views/shared/snippets/_form.html.haml
+++ b/app/views/shared/snippets/_form.html.haml
@@ -1,6 +1,5 @@
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('lib/ace.js')
- = webpack_bundle_tag('snippet')
.snippet-form-holder
= form_for @snippet, url: url, html: { class: "form-horizontal snippet-form js-requires-input js-quick-submit common-note-form" } do |f|
diff --git a/app/views/shared/snippets/_snippet.html.haml b/app/views/shared/snippets/_snippet.html.haml
index 491a8a41090..3acec88c2e3 100644
--- a/app/views/shared/snippets/_snippet.html.haml
+++ b/app/views/shared/snippets/_snippet.html.haml
@@ -31,7 +31,7 @@
%span.hidden-xs
in
= link_to project_path(snippet.project) do
- = snippet.project.name_with_namespace
+ = snippet.project.full_name
.pull-right.snippet-updated-at
%span updated #{time_ago_with_tooltip(snippet.updated_at, placement: 'bottom')}
diff --git a/app/views/u2f/_authenticate.html.haml b/app/views/u2f/_authenticate.html.haml
index f878bece2fa..7eb221620ad 100644
--- a/app/views/u2f/_authenticate.html.haml
+++ b/app/views/u2f/_authenticate.html.haml
@@ -1,6 +1,7 @@
#js-authenticate-u2f
%a.btn.btn-block.btn-info#js-login-2fa-device{ href: '#' } Sign in via 2FA code
+-# haml-lint:disable InlineJavaScript
%script#js-authenticate-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index 79e8f8d0e89..cc0e93c0755 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -1,5 +1,6 @@
#js-register-u2f
+-# haml-lint:disable InlineJavaScript
%script#js-register-u2f-not-supported{ type: "text/template" }
%p Your browser doesn't support U2F. Please use Google Chrome desktop (version 41 or newer).
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 28a5e5da037..328db19be29 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -24,6 +24,7 @@
- gcp_cluster:cluster_wait_for_app_installation
- gcp_cluster:wait_for_cluster_creation
- gcp_cluster:check_gcp_project_billing
+- gcp_cluster:cluster_wait_for_ingress_ip_address
- github_import_advance_stage
- github_importer:github_import_import_diff_note
@@ -84,6 +85,7 @@
- new_note
- pages
- pages_domain_verification
+- plugin
- post_receive
- process_commit
- project_cache
diff --git a/app/workers/authorized_projects_worker.rb b/app/workers/authorized_projects_worker.rb
index 09559e3b696..d7e24491516 100644
--- a/app/workers/authorized_projects_worker.rb
+++ b/app/workers/authorized_projects_worker.rb
@@ -1,42 +1,10 @@
class AuthorizedProjectsWorker
include ApplicationWorker
+ prepend WaitableWorker
- # Schedules multiple jobs and waits for them to be completed.
- def self.bulk_perform_and_wait(args_list)
- # Short-circuit: it's more efficient to do small numbers of jobs inline
- return bulk_perform_inline(args_list) if args_list.size <= 3
-
- waiter = Gitlab::JobWaiter.new(args_list.size)
-
- # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
- # into [[1, "key"], [2, "key"], [3, "key"]]
- waiting_args_list = args_list.map { |args| [*args, waiter.key] }
- bulk_perform_async(waiting_args_list)
-
- waiter.wait
- end
-
- # Performs multiple jobs directly. Failed jobs will be put into sidekiq so
- # they can benefit from retries
- def self.bulk_perform_inline(args_list)
- failed = []
-
- args_list.each do |args|
- begin
- new.perform(*args)
- rescue
- failed << args
- end
- end
-
- bulk_perform_async(failed) if failed.present?
- end
-
- def perform(user_id, notify_key = nil)
+ def perform(user_id)
user = User.find_by(id: user_id)
user&.refresh_authorized_projects
- ensure
- Gitlab::JobWaiter.notify(notify_key, jid) if notify_key
end
end
diff --git a/app/workers/cluster_wait_for_ingress_ip_address_worker.rb b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
new file mode 100644
index 00000000000..8ba5951750c
--- /dev/null
+++ b/app/workers/cluster_wait_for_ingress_ip_address_worker.rb
@@ -0,0 +1,11 @@
+class ClusterWaitForIngressIpAddressWorker
+ include ApplicationWorker
+ include ClusterQueue
+ include ClusterApplications
+
+ def perform(app_name, app_id)
+ find_application(app_name, app_id) do |app|
+ Clusters::Applications::CheckIngressIpAddressService.new(app).execute
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index 9a9fbaad653..100d86e38c8 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -22,7 +22,7 @@ module Gitlab
importer_class.new(object, project, client).execute
- counter.increment(project: project.path_with_namespace)
+ counter.increment(project: project.full_path)
end
def counter
diff --git a/app/workers/concerns/waitable_worker.rb b/app/workers/concerns/waitable_worker.rb
new file mode 100644
index 00000000000..48ebe862248
--- /dev/null
+++ b/app/workers/concerns/waitable_worker.rb
@@ -0,0 +1,44 @@
+module WaitableWorker
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ # Schedules multiple jobs and waits for them to be completed.
+ def bulk_perform_and_wait(args_list, timeout: 10)
+ # Short-circuit: it's more efficient to do small numbers of jobs inline
+ return bulk_perform_inline(args_list) if args_list.size <= 3
+
+ waiter = Gitlab::JobWaiter.new(args_list.size)
+
+ # Point all the bulk jobs at the same JobWaiter. Converts, [[1], [2], [3]]
+ # into [[1, "key"], [2, "key"], [3, "key"]]
+ waiting_args_list = args_list.map { |args| [*args, waiter.key] }
+ bulk_perform_async(waiting_args_list)
+
+ waiter.wait(timeout)
+ end
+
+ # Performs multiple jobs directly. Failed jobs will be put into sidekiq so
+ # they can benefit from retries
+ def bulk_perform_inline(args_list)
+ failed = []
+
+ args_list.each do |args|
+ begin
+ new.perform(*args)
+ rescue
+ failed << args
+ end
+ end
+
+ bulk_perform_async(failed) if failed.present?
+ end
+ end
+
+ def perform(*args)
+ notify_key = args.pop if Gitlab::JobWaiter.key?(args.last)
+
+ super(*args)
+ ensure
+ Gitlab::JobWaiter.notify(notify_key, jid) if notify_key
+ end
+end
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index 7ba224d74c8..55fb817ca6e 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -44,6 +44,10 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
+
+ # In case pack files are deleted, release libgit2 cache and open file
+ # descriptors ASAP instead of waiting for Ruby garbage collection
+ project.cleanup
ensure
cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
end
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index 073d6608082..a779e631516 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -16,7 +16,7 @@ module Gitlab
def report_import_time(project)
duration = Time.zone.now - project.created_at
- path = project.path_with_namespace
+ path = project.full_path
histogram.observe({ project: path }, duration)
counter.increment
diff --git a/app/workers/pages_worker.rb b/app/workers/pages_worker.rb
index d3b95009364..66a0ff83bef 100644
--- a/app/workers/pages_worker.rb
+++ b/app/workers/pages_worker.rb
@@ -1,7 +1,7 @@
class PagesWorker
include ApplicationWorker
- sidekiq_options retry: false
+ sidekiq_options retry: 3
def perform(action, *arg)
send(action, *arg) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/workers/plugin_worker.rb b/app/workers/plugin_worker.rb
new file mode 100644
index 00000000000..bfcc683d99a
--- /dev/null
+++ b/app/workers/plugin_worker.rb
@@ -0,0 +1,15 @@
+class PluginWorker
+ include ApplicationWorker
+
+ sidekiq_options retry: false
+
+ def perform(file_name, data)
+ success, message = Gitlab::Plugin.execute(file_name, data)
+
+ unless success
+ Gitlab::PluginLogger.error("Plugin Error => #{file_name}: #{message}")
+ end
+
+ true
+ end
+end
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 5b25d980bdb..201e7f332b4 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -30,10 +30,9 @@ class ProcessCommitWorker
end
def process_commit_message(project, commit, user, author, default = false)
- # this is a GitLab generated commit message, ignore it.
- return if commit.merged_merge_request?(user)
-
- closed_issues = default ? commit.closes_issues(user) : []
+ # Ignore closing references from GitLab-generated commit messages.
+ find_closing_issues = default && !commit.merged_merge_request?(user)
+ closed_issues = find_closing_issues ? commit.closes_issues(user) : []
close_issues(project, user, author, commit, closed_issues) if closed_issues.any?
commit.create_cross_references!(author, closed_issues)
diff --git a/app/workers/remove_expired_members_worker.rb b/app/workers/remove_expired_members_worker.rb
index d80b3b15840..68960f72bf6 100644
--- a/app/workers/remove_expired_members_worker.rb
+++ b/app/workers/remove_expired_members_worker.rb
@@ -5,7 +5,7 @@ class RemoveExpiredMembersWorker
def perform
Member.expired.find_each do |member|
begin
- Members::AuthorizedDestroyService.new(member).execute
+ Members::DestroyService.new.execute(member, skip_authorization: true)
rescue => ex
logger.error("Expired Member ID=#{member.id} cannot be removed - #{ex}")
end
diff --git a/changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml b/changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml
new file mode 100644
index 00000000000..ca049f9edaa
--- /dev/null
+++ b/changelogs/unreleased/17359-move-oauth-modules-to-auth-dir-structure.yml
@@ -0,0 +1,4 @@
+---
+title: Moved o_auth/saml/ldap modules under gitlab/auth
+merge_request: 17359
+author: Horatiu Eugen Vlad
diff --git a/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml b/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml
new file mode 100644
index 00000000000..b909bb2d021
--- /dev/null
+++ b/changelogs/unreleased/24774-clear-the-Labels-dropdown-search-filter.yml
@@ -0,0 +1,5 @@
+---
+title: Clear the Labels dropdown search filter after a selection is made
+merge_request: 17393
+author: Andrew Torres
+type: changed
diff --git a/changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml b/changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml
new file mode 100644
index 00000000000..175b3103d90
--- /dev/null
+++ b/changelogs/unreleased/30665-add-email-button-to-new-issue-by-email.yml
@@ -0,0 +1,4 @@
+---
+title: Add email button to new issue by email
+merge_request: 10942
+author: Islam Wazery
diff --git a/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml b/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml
new file mode 100644
index 00000000000..74675992105
--- /dev/null
+++ b/changelogs/unreleased/32831-single-deploy-of-runner-in-k8s-cluster.yml
@@ -0,0 +1,5 @@
+---
+title: Allow installation of GitLab Runner with a single click
+merge_request: 17134
+author:
+type: added
diff --git a/changelogs/unreleased/33570-slack-notify-default-branch.yml b/changelogs/unreleased/33570-slack-notify-default-branch.yml
new file mode 100644
index 00000000000..5c90ce47729
--- /dev/null
+++ b/changelogs/unreleased/33570-slack-notify-default-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Slack/Mattermost notifications not respecting `notify_only_default_branch` setting for pushes
+merge_request: 17345
+author:
+type: fixed
diff --git a/changelogs/unreleased/34130-null-pipes.yml b/changelogs/unreleased/34130-null-pipes.yml
deleted file mode 100644
index a56e5cf8db2..00000000000
--- a/changelogs/unreleased/34130-null-pipes.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Prevent MR Widget error when no CI configured
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/38587-pipelines-empty-state.yml b/changelogs/unreleased/38587-pipelines-empty-state.yml
new file mode 100644
index 00000000000..58ea204d394
--- /dev/null
+++ b/changelogs/unreleased/38587-pipelines-empty-state.yml
@@ -0,0 +1,5 @@
+---
+title: Handle empty state in Pipelines page
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml b/changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml
new file mode 100644
index 00000000000..3833aab42dd
--- /dev/null
+++ b/changelogs/unreleased/40187-project-branch-dashboard-with-active-stale-branches.yml
@@ -0,0 +1,5 @@
+---
+title: Add overview of branches and a filter for active/stale branches
+merge_request: 15402
+author: Takuya Noguchi
+type: added
diff --git a/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml b/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml
new file mode 100644
index 00000000000..dddd8473df5
--- /dev/null
+++ b/changelogs/unreleased/40502-osw-keep-link-when-redacting-unauthorized-objects.yml
@@ -0,0 +1,5 @@
+---
+title: Keep link when redacting unauthorized object links
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/41461-project-members-slow-due-to-sql.yml b/changelogs/unreleased/41461-project-members-slow-due-to-sql.yml
deleted file mode 100644
index 27eee7d943b..00000000000
--- a/changelogs/unreleased/41461-project-members-slow-due-to-sql.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve query performance of MembersFinder.
-merge_request: 17190
-author:
-type: performance
diff --git a/changelogs/unreleased/41616-api-issues-between-date.yml b/changelogs/unreleased/41616-api-issues-between-date.yml
new file mode 100644
index 00000000000..d8a23f48699
--- /dev/null
+++ b/changelogs/unreleased/41616-api-issues-between-date.yml
@@ -0,0 +1,5 @@
+---
+title: Adds updated_at filter to issues and merge_requests API
+merge_request: 17417
+author: Jacopo Beschi @jacopo-beschi
+type: added
diff --git a/changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml b/changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml
deleted file mode 100644
index 507367c98c4..00000000000
--- a/changelogs/unreleased/41619-turn-on-legacy-authorization-for-new-clusters-on-gke.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Enable Legacy Authorization by default on Cluster creations
-merge_request: 17302
-author:
-type: fixed
diff --git a/changelogs/unreleased/41719-mr-title-fix.yml b/changelogs/unreleased/41719-mr-title-fix.yml
new file mode 100644
index 00000000000..92388f30cb2
--- /dev/null
+++ b/changelogs/unreleased/41719-mr-title-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Render htmlentities correctly for links not supported by Rinku
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml b/changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml
new file mode 100644
index 00000000000..8d8a5dfefa3
--- /dev/null
+++ b/changelogs/unreleased/41777-include-cycle-time-in-usage-ping.yml
@@ -0,0 +1,5 @@
+---
+title: Include cycle time in usage ping data
+merge_request: 16973
+author:
+type: added
diff --git a/changelogs/unreleased/41851-enable-eslint-codeclimate.yml b/changelogs/unreleased/41851-enable-eslint-codeclimate.yml
new file mode 100644
index 00000000000..98924f3eae8
--- /dev/null
+++ b/changelogs/unreleased/41851-enable-eslint-codeclimate.yml
@@ -0,0 +1,5 @@
+---
+title: Enables eslint in codeclimate job
+merge_request: 17392
+author:
+type: other
diff --git a/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml b/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml
new file mode 100644
index 00000000000..c9e23360e3b
--- /dev/null
+++ b/changelogs/unreleased/41905_merge_request_and_issue_metrics.yml
@@ -0,0 +1,5 @@
+---
+title: expose more metrics in merge requests api
+merge_request: 16589
+author: haseebeqx
+type: added
diff --git a/changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml b/changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml
new file mode 100644
index 00000000000..c596a88ba0b
--- /dev/null
+++ b/changelogs/unreleased/42434-allow-commits-endpoint-to-work-over-all-commits.yml
@@ -0,0 +1,5 @@
+---
+title: Allow commits endpoint to work over all commits of a repository
+merge_request: 17182
+author:
+type: added
diff --git a/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml b/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml
new file mode 100644
index 00000000000..35457db82f4
--- /dev/null
+++ b/changelogs/unreleased/42643-persist-external-ip-of-ingress-controller-gke.yml
@@ -0,0 +1,5 @@
+---
+title: Display ingress IP address in the Kubernetes page
+merge_request: 17052
+author:
+type: added
diff --git a/changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml b/changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml
new file mode 100644
index 00000000000..609b5ce48ef
--- /dev/null
+++ b/changelogs/unreleased/42712_api_branches_add_search_param_20180207.yml
@@ -0,0 +1,5 @@
+---
+title: Add search param to Branches API
+merge_request: 17005
+author: bunufi
+type: added
diff --git a/changelogs/unreleased/42877-snippets-dashboard-slow.yml b/changelogs/unreleased/42877-snippets-dashboard-slow.yml
deleted file mode 100644
index 839b44ad272..00000000000
--- a/changelogs/unreleased/42877-snippets-dashboard-slow.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve query performance for snippets dashboard.
-merge_request: 17088
-author:
-type: performance
diff --git a/changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml b/changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml
new file mode 100644
index 00000000000..0e566dd0abf
--- /dev/null
+++ b/changelogs/unreleased/42946-update-pipeline-cancel-tooltip-to-stop.yml
@@ -0,0 +1,5 @@
+---
+title: Update tooltip on pipeline cancel to Stop (#42946)
+merge_request: 17444
+author:
+type: fixed
diff --git a/changelogs/unreleased/43261-fix-prometheus-installation.yml b/changelogs/unreleased/43261-fix-prometheus-installation.yml
new file mode 100644
index 00000000000..b5fc7980390
--- /dev/null
+++ b/changelogs/unreleased/43261-fix-prometheus-installation.yml
@@ -0,0 +1,5 @@
+---
+title: Allow Prometheus application to be installed from Cluster applications
+merge_request: 17372
+author:
+type: fixed
diff --git a/changelogs/unreleased/43275-improve-variables-validation-message.yml b/changelogs/unreleased/43275-improve-variables-validation-message.yml
new file mode 100644
index 00000000000..88ef93123a0
--- /dev/null
+++ b/changelogs/unreleased/43275-improve-variables-validation-message.yml
@@ -0,0 +1,5 @@
+---
+title: Remove duplicated error message on duplicate variable validation
+merge_request: 17135
+author:
+type: fixed
diff --git a/changelogs/unreleased/43315-gpg-popover.yml b/changelogs/unreleased/43315-gpg-popover.yml
new file mode 100644
index 00000000000..69238aa8075
--- /dev/null
+++ b/changelogs/unreleased/43315-gpg-popover.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes gpg popover layout
+merge_request: 17323
+author:
+type: fixed
diff --git a/changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml b/changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml
new file mode 100644
index 00000000000..86be5ee1804
--- /dev/null
+++ b/changelogs/unreleased/43334-reply-by-email-did-not-pick-up-unsubscribe-quick-action.yml
@@ -0,0 +1,5 @@
+---
+title: Fix quick actions for users who cannot update issues and merge requests
+merge_request: 17482
+author:
+type: fixed
diff --git a/changelogs/unreleased/43373-fix-cache-index-appending.yml b/changelogs/unreleased/43373-fix-cache-index-appending.yml
deleted file mode 100644
index fdb293ea04d..00000000000
--- a/changelogs/unreleased/43373-fix-cache-index-appending.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix issue with cache key being empty when variable used as the key
-merge_request: 17260
-author:
-type: fixed
diff --git a/changelogs/unreleased/43489-display-runner-ip.yml b/changelogs/unreleased/43489-display-runner-ip.yml
new file mode 100644
index 00000000000..621c2ec709a
--- /dev/null
+++ b/changelogs/unreleased/43489-display-runner-ip.yml
@@ -0,0 +1,5 @@
+---
+title: Display Runner IP Address
+merge_request: 17286
+author:
+type: added
diff --git a/changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml b/changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml
new file mode 100644
index 00000000000..e163c04f430
--- /dev/null
+++ b/changelogs/unreleased/43510-merge-requests-and-issues-don-t-show-for-all-subgroups.yml
@@ -0,0 +1,6 @@
+---
+title: Ensure group issues and merge requests pages show results from subgroups when
+ there are no results from the current group
+merge_request: 17312
+author:
+type: fixed
diff --git a/changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml b/changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml
new file mode 100644
index 00000000000..173710412a5
--- /dev/null
+++ b/changelogs/unreleased/43531-500-error-searching-wiki-incompatible-character-encodings-utf-8-and-ascii-8bit.yml
@@ -0,0 +1,5 @@
+---
+title: Fix code and wiki search results pages when non-ASCII text is displayed
+merge_request: 17413
+author:
+type: fixed
diff --git a/changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml b/changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml
new file mode 100644
index 00000000000..25bcbf2fbab
--- /dev/null
+++ b/changelogs/unreleased/43532-error-on-admin-applications-prometheus-template.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes Prometheus admin configuration page
+merge_request: 17377
+author:
+type: fixed
diff --git a/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml b/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml
new file mode 100644
index 00000000000..bda4ec84e5c
--- /dev/null
+++ b/changelogs/unreleased/43598-fix-duplicate-label-load-failure.yml
@@ -0,0 +1,5 @@
+---
+title: Fix Group labels load failure when there are duplicate labels present
+merge_request: 17353
+author:
+type: fixed
diff --git a/changelogs/unreleased/43643-fix-mr-label-filtering.yml b/changelogs/unreleased/43643-fix-mr-label-filtering.yml
new file mode 100644
index 00000000000..32a44aef243
--- /dev/null
+++ b/changelogs/unreleased/43643-fix-mr-label-filtering.yml
@@ -0,0 +1,5 @@
+---
+title: Enable filtering MR list based on clicked label in MR sidebar
+merge_request: 17390
+author:
+type: fixed
diff --git a/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml b/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml
new file mode 100644
index 00000000000..08109632e8e
--- /dev/null
+++ b/changelogs/unreleased/43793-enable-privileged-mode-for-runner.yml
@@ -0,0 +1,5 @@
+---
+title: Enable privileged mode for GitLab Runner
+merge_request: 17528
+author:
+type: added
diff --git a/changelogs/unreleased/43829-update-ssh-addtion-text.yml b/changelogs/unreleased/43829-update-ssh-addtion-text.yml
new file mode 100644
index 00000000000..b7052bb171e
--- /dev/null
+++ b/changelogs/unreleased/43829-update-ssh-addtion-text.yml
@@ -0,0 +1,5 @@
+---
+title: Update SSH key link to include existing keys
+merge_request:
+author: Brendan O'Leary
+type: changed
diff --git a/changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml b/changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml
new file mode 100644
index 00000000000..526523964c3
--- /dev/null
+++ b/changelogs/unreleased/43837-error-handle-in-updating-milestone-on-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Stop loading spinner on error of milestone update on issue
+merge_request: 17507
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml b/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml
new file mode 100644
index 00000000000..c0fa8e2e377
--- /dev/null
+++ b/changelogs/unreleased/4826-create-empty-wiki-when-it-s-enabled.yml
@@ -0,0 +1,5 @@
+---
+title: Make sure wiki exists when it's enabled
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/an-network-controller-fix.yml b/changelogs/unreleased/an-network-controller-fix.yml
new file mode 100644
index 00000000000..cb2c447b957
--- /dev/null
+++ b/changelogs/unreleased/an-network-controller-fix.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent the graphs page from generating unnecessary Gitaly requests
+merge_request: 37602
+author:
+type: performance
diff --git a/changelogs/unreleased/an-workhorse-3-8-0.yml b/changelogs/unreleased/an-workhorse-3-8-0.yml
new file mode 100644
index 00000000000..5e2a72e1eda
--- /dev/null
+++ b/changelogs/unreleased/an-workhorse-3-8-0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade Workhorse to version 3.8.0 to support structured logging
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/assignees-vue-component-missing-data-container.yml b/changelogs/unreleased/assignees-vue-component-missing-data-container.yml
new file mode 100644
index 00000000000..233d983b415
--- /dev/null
+++ b/changelogs/unreleased/assignees-vue-component-missing-data-container.yml
@@ -0,0 +1,5 @@
+---
+title: Add Assignees vue component missing data container
+merge_request: 17426
+author: George Tsiolis
+type: fixed
diff --git a/changelogs/unreleased/dz-namespace-id-not-null.yml b/changelogs/unreleased/dz-namespace-id-not-null.yml
new file mode 100644
index 00000000000..07b32aeeb86
--- /dev/null
+++ b/changelogs/unreleased/dz-namespace-id-not-null.yml
@@ -0,0 +1,5 @@
+---
+title: Add NOT NULL constraint to projects.namespace_id
+merge_request: 17448
+author:
+type: other
diff --git a/changelogs/unreleased/dz-system-hooks-plugins.yml b/changelogs/unreleased/dz-system-hooks-plugins.yml
new file mode 100644
index 00000000000..e6eb1dfb03b
--- /dev/null
+++ b/changelogs/unreleased/dz-system-hooks-plugins.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to use external plugins as an alternative to system hooks
+merge_request: 17003
+author:
+type: added
diff --git a/changelogs/unreleased/ee-4862-verify-file-checksums.yml b/changelogs/unreleased/ee-4862-verify-file-checksums.yml
new file mode 100644
index 00000000000..392c766ab37
--- /dev/null
+++ b/changelogs/unreleased/ee-4862-verify-file-checksums.yml
@@ -0,0 +1,5 @@
+---
+title: Foreground verification of uploads and LFS objects
+merge_request: 17402
+author:
+type: added
diff --git a/changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml b/changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml
new file mode 100644
index 00000000000..768686aeda8
--- /dev/null
+++ b/changelogs/unreleased/feature--43691-count-diff-note-calendar-activity.yml
@@ -0,0 +1,5 @@
+---
+title: Count comments on diffs as contributions for the contributions calendar
+merge_request: 17418
+author: Riccardo Padovani
+type: fixed
diff --git a/changelogs/unreleased/feature-edit_pages_domain.yml b/changelogs/unreleased/feature-edit_pages_domain.yml
new file mode 100644
index 00000000000..bd0af53296c
--- /dev/null
+++ b/changelogs/unreleased/feature-edit_pages_domain.yml
@@ -0,0 +1,5 @@
+---
+title: 'Pages custom domain: allow update of key/certificate'
+merge_request: 17376
+author: rfwatson
+type: changed
diff --git a/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml b/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml
new file mode 100644
index 00000000000..28820649af3
--- /dev/null
+++ b/changelogs/unreleased/feature-gb-pipeline-variable-expressions.yml
@@ -0,0 +1,5 @@
+---
+title: Add catch-up background migration to migrate pipeline stages
+merge_request: 15741
+author:
+type: performance
diff --git a/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml b/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml
new file mode 100644
index 00000000000..23a870d6e9f
--- /dev/null
+++ b/changelogs/unreleased/feature-sm-add-check-sum-to-job-artifacts.yml
@@ -0,0 +1,5 @@
+---
+title: Store sha256 checksum to job artifacts
+merge_request: 17354
+author:
+type: performance
diff --git a/changelogs/unreleased/fix-500-for-invalid-upload-path.yml b/changelogs/unreleased/fix-500-for-invalid-upload-path.yml
deleted file mode 100644
index a4ce00c64c4..00000000000
--- a/changelogs/unreleased/fix-500-for-invalid-upload-path.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix 500 error when loading an invalid upload URL
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/fix-sm-fix_pages_worker.yml b/changelogs/unreleased/fix-sm-fix_pages_worker.yml
new file mode 100644
index 00000000000..190c7d3e83e
--- /dev/null
+++ b/changelogs/unreleased/fix-sm-fix_pages_worker.yml
@@ -0,0 +1,5 @@
+---
+title: Fix pages flaky failure by reloading stale object
+merge_request: 17522
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-28141-redirection-loop.yml b/changelogs/unreleased/fj-28141-redirection-loop.yml
new file mode 100644
index 00000000000..db7e109a06e
--- /dev/null
+++ b/changelogs/unreleased/fj-28141-redirection-loop.yml
@@ -0,0 +1,5 @@
+---
+title: Removing the two factor check when the user sets a new password
+merge_request: 17457
+author:
+type: fixed
diff --git a/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml b/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml
new file mode 100644
index 00000000000..7cb12e26332
--- /dev/null
+++ b/changelogs/unreleased/fj-41174-projects-groups-badges-api.yml
@@ -0,0 +1,5 @@
+---
+title: Implemented badge API endpoints
+merge_request: 17082
+author:
+type: added
diff --git a/changelogs/unreleased/flipper-caching.yml b/changelogs/unreleased/flipper-caching.yml
deleted file mode 100644
index 6db27fd579e..00000000000
--- a/changelogs/unreleased/flipper-caching.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Increase feature flag cache TTL to one hour
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/grpc-unavailable-restart.yml b/changelogs/unreleased/grpc-unavailable-restart.yml
new file mode 100644
index 00000000000..5ce08d66004
--- /dev/null
+++ b/changelogs/unreleased/grpc-unavailable-restart.yml
@@ -0,0 +1,5 @@
+---
+title: Restart Unicorn and Sidekiq when GRPC throws 14:Endpoint read failed
+merge_request: 17293
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue-edit-shortcut.yml b/changelogs/unreleased/issue-edit-shortcut.yml
new file mode 100644
index 00000000000..2b29b2bc03f
--- /dev/null
+++ b/changelogs/unreleased/issue-edit-shortcut.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed issue edit shortcut not opening edit form
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/issue_31081.yml b/changelogs/unreleased/issue_31081.yml
new file mode 100644
index 00000000000..ac547c285db
--- /dev/null
+++ b/changelogs/unreleased/issue_31081.yml
@@ -0,0 +1,5 @@
+---
+title: Use host URL to build JIRA remote link icon
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml b/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml
deleted file mode 100644
index 09112fba85e..00000000000
--- a/changelogs/unreleased/jej-fix-slow-lfs-object-check.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Only check LFS integrity for first ref in a push to avoid timeout
-merge_request: 17098
-author:
-type: performance
diff --git a/changelogs/unreleased/jprovazn-scoped-limit.yml b/changelogs/unreleased/jprovazn-scoped-limit.yml
new file mode 100644
index 00000000000..45724bb3479
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-scoped-limit.yml
@@ -0,0 +1,6 @@
+---
+title: Optimize search queries on the search page by setting a limit for matching
+ records in project scope
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml b/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml
deleted file mode 100644
index 690536a533b..00000000000
--- a/changelogs/unreleased/kp-fix-stacked-bar-progress-value-clipping.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Fix single digit value clipping for stacked progress bar
-merge_request: 17217
-author:
-type: fixed
diff --git a/changelogs/unreleased/kp-label-select-vue.yml b/changelogs/unreleased/kp-label-select-vue.yml
new file mode 100644
index 00000000000..1f5952f2554
--- /dev/null
+++ b/changelogs/unreleased/kp-label-select-vue.yml
@@ -0,0 +1,5 @@
+---
+title: Port Labels Select dropdown to Vue
+merge_request: 17411
+author:
+type: other
diff --git a/changelogs/unreleased/merge-requests-api-filter-by-branch.yml b/changelogs/unreleased/merge-requests-api-filter-by-branch.yml
new file mode 100644
index 00000000000..03a7e4d0f71
--- /dev/null
+++ b/changelogs/unreleased/merge-requests-api-filter-by-branch.yml
@@ -0,0 +1,5 @@
+---
+title: Add support for filtering by source and target branch to merge requests API
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/minimal-fix-for-artifacts-service.yml b/changelogs/unreleased/minimal-fix-for-artifacts-service.yml
new file mode 100644
index 00000000000..11f5bc17759
--- /dev/null
+++ b/changelogs/unreleased/minimal-fix-for-artifacts-service.yml
@@ -0,0 +1,5 @@
+---
+title: Prevent trace artifact migration to incur data loss
+merge_request: 17313
+author:
+type: fixed
diff --git a/changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml b/changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml
new file mode 100644
index 00000000000..a761d610da1
--- /dev/null
+++ b/changelogs/unreleased/mk-fix-error-code-for-repo-does-not-exist.yml
@@ -0,0 +1,5 @@
+---
+title: Return a 404 instead of 403 if the repository does not exist on disk
+merge_request: 17341
+author:
+type: fixed
diff --git a/changelogs/unreleased/oauth_generic_provider.yml b/changelogs/unreleased/oauth_generic_provider.yml
new file mode 100644
index 00000000000..3b6f8b04529
--- /dev/null
+++ b/changelogs/unreleased/oauth_generic_provider.yml
@@ -0,0 +1,4 @@
+---
+title: Make oauth provider login generic
+merge_request: 8809
+author: Horatiu Eugen Vlad \ No newline at end of file
diff --git a/changelogs/unreleased/refactor-move-assignees-vue-component.yml b/changelogs/unreleased/refactor-move-assignees-vue-component.yml
new file mode 100644
index 00000000000..98cfa6b4c81
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-assignees-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move Assignees vue component
+merge_request: 16952
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml b/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml
new file mode 100644
index 00000000000..20d05530513
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-board-new-issue-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move BoardNewIssue vue component
+merge_request: 16947
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml b/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml
new file mode 100644
index 00000000000..d65318d7ba1
--- /dev/null
+++ b/changelogs/unreleased/refactor-move-filtered-search-vue-component.yml
@@ -0,0 +1,5 @@
+---
+title: Move RecentSearchesDropdownContent vue component
+merge_request: 16951
+author: George Tsiolis
+type: performance
diff --git a/changelogs/unreleased/remove-projects-finder-from-todos-finder.yml b/changelogs/unreleased/remove-projects-finder-from-todos-finder.yml
new file mode 100644
index 00000000000..0a3fc751edb
--- /dev/null
+++ b/changelogs/unreleased/remove-projects-finder-from-todos-finder.yml
@@ -0,0 +1,5 @@
+---
+title: Don't use ProjectsFinder in TodosFinder
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/revert-project-visibility-changes.yml b/changelogs/unreleased/revert-project-visibility-changes.yml
new file mode 100644
index 00000000000..df44fdb79b1
--- /dev/null
+++ b/changelogs/unreleased/revert-project-visibility-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Revert Project.public_or_visible_to_user changes and only apply to snippets
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-cleanup-after-git-gc.yml b/changelogs/unreleased/sh-cleanup-after-git-gc.yml
new file mode 100644
index 00000000000..4b652f4d6ce
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-after-git-gc.yml
@@ -0,0 +1,5 @@
+---
+title: Release libgit2 cache and open file descriptors after `git gc` run
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml b/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml
new file mode 100644
index 00000000000..7c7ef39cb75
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-issue-43871-system-hooks.yml
@@ -0,0 +1,5 @@
+---
+title: Don't error out in system hook if user has `nil` datetime columns
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-guard-read-only-user-updates.yml b/changelogs/unreleased/sh-guard-read-only-user-updates.yml
deleted file mode 100644
index b8dbd840ed9..00000000000
--- a/changelogs/unreleased/sh-guard-read-only-user-updates.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Don't attempt to update user tracked fields if database is in read-only
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/users-autocomplete.yml b/changelogs/unreleased/users-autocomplete.yml
deleted file mode 100644
index 2cb078a3a7c..00000000000
--- a/changelogs/unreleased/users-autocomplete.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Improve performance of searching for and autocompleting of users
-merge_request:
-author:
-type: performance
diff --git a/changelogs/unreleased/wip-new-mr-cmd.yml b/changelogs/unreleased/wip-new-mr-cmd.yml
new file mode 100644
index 00000000000..e930758ec9d
--- /dev/null
+++ b/changelogs/unreleased/wip-new-mr-cmd.yml
@@ -0,0 +1,5 @@
+---
+title: Port /wip quick action command to Merge Request creation (on description)
+merge_request: 17463
+author: Adam Pahlevi
+type: added
diff --git a/changelogs/unreleased/zj-branch-contains-git-message.yml b/changelogs/unreleased/zj-branch-contains-git-message.yml
deleted file mode 100644
index ce034e7ec87..00000000000
--- a/changelogs/unreleased/zj-branch-contains-git-message.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Allow branch names to be named the same as the sha it points to
-merge_request:
-author:
-type: fixed
diff --git a/changelogs/unreleased/zj-gitaly-encoding-issue.yml b/changelogs/unreleased/zj-gitaly-encoding-issue.yml
new file mode 100644
index 00000000000..073d8f38e4b
--- /dev/null
+++ b/changelogs/unreleased/zj-gitaly-encoding-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Encode branch name as binary before creating a RPC request to copy attributes
+merge_request: 17291
+author:
+type: fixed
diff --git a/changelogs/unreleased/zj-version-string-grouping-ci.yml b/changelogs/unreleased/zj-version-string-grouping-ci.yml
new file mode 100644
index 00000000000..04ef0f65b1e
--- /dev/null
+++ b/changelogs/unreleased/zj-version-string-grouping-ci.yml
@@ -0,0 +1,5 @@
+---
+title: Allow CI/CD Jobs being grouped on version strings
+merge_request:
+author:
+type: added
diff --git a/config.ru b/config.ru
index de0400f4f67..7b15939c6ff 100644
--- a/config.ru
+++ b/config.ru
@@ -23,5 +23,6 @@ warmup do |app|
end
map ENV['RAILS_RELATIVE_URL_ROOT'] || "/" do
+ use Gitlab::Middleware::ReleaseEnv
run Gitlab::Application
end
diff --git a/config/application.rb b/config/application.rb
index 918bd4d57cf..74fe3e439ed 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -26,6 +26,7 @@ module Gitlab
# This is a nice reference article on autoloading/eager loading:
# http://blog.arkency.com/2014/11/dont-forget-about-eager-load-when-extending-autoload
config.eager_load_paths.push(*%W[#{config.root}/lib
+ #{config.root}/app/models/badges
#{config.root}/app/models/hooks
#{config.root}/app/models/members
#{config.root}/app/models/project_services
diff --git a/config/initializers/devise.rb b/config/initializers/devise.rb
index fa25f3778fa..f642e6d47e0 100644
--- a/config/initializers/devise.rb
+++ b/config/initializers/devise.rb
@@ -212,9 +212,9 @@ Devise.setup do |config|
# manager.default_strategies(scope: :user).unshift :some_external_strategy
# end
- if Gitlab::LDAP::Config.enabled?
- Gitlab::LDAP::Config.providers.each do |provider|
- ldap_config = Gitlab::LDAP::Config.new(provider)
+ if Gitlab::Auth::LDAP::Config.enabled?
+ Gitlab::Auth::LDAP::Config.providers.each do |provider|
+ ldap_config = Gitlab::Auth::LDAP::Config.new(provider)
config.omniauth(provider, ldap_config.omniauth_options)
end
end
@@ -235,9 +235,9 @@ Devise.setup do |config|
if provider['name'] == 'cas3'
provider['args'][:on_single_sign_out] = lambda do |request|
ticket = request.params[:session_index]
- raise "Service Ticket not found." unless Gitlab::OAuth::Session.valid?(:cas3, ticket)
+ raise "Service Ticket not found." unless Gitlab::Auth::OAuth::Session.valid?(:cas3, ticket)
- Gitlab::OAuth::Session.destroy(:cas3, ticket)
+ Gitlab::Auth::OAuth::Session.destroy(:cas3, ticket)
true
end
end
@@ -245,8 +245,8 @@ Devise.setup do |config|
if provider['name'] == 'authentiq'
provider['args'][:remote_sign_out_handler] = lambda do |request|
authentiq_session = request.params['sid']
- if Gitlab::OAuth::Session.valid?(:authentiq, authentiq_session)
- Gitlab::OAuth::Session.destroy(:authentiq, authentiq_session)
+ if Gitlab::Auth::OAuth::Session.valid?(:authentiq, authentiq_session)
+ Gitlab::Auth::OAuth::Session.destroy(:authentiq, authentiq_session)
true
else
false
diff --git a/config/initializers/doorkeeper.rb b/config/initializers/doorkeeper.rb
index b89f0419b91..2079d3acb72 100644
--- a/config/initializers/doorkeeper.rb
+++ b/config/initializers/doorkeeper.rb
@@ -103,4 +103,6 @@ Doorkeeper.configure do
# Some applications require dynamic query parameters on their request_uri
# set to true if you want this to be allowed
# wildcard_redirect_uri false
+
+ base_controller 'ApplicationController'
end
diff --git a/config/initializers/forbid_sidekiq_in_transactions.rb b/config/initializers/forbid_sidekiq_in_transactions.rb
index cb611aa21df..4cf1d455eb4 100644
--- a/config/initializers/forbid_sidekiq_in_transactions.rb
+++ b/config/initializers/forbid_sidekiq_in_transactions.rb
@@ -18,13 +18,26 @@ module Sidekiq
%i(perform_async perform_at perform_in).each do |name|
define_method(name) do |*args|
if !Sidekiq::Worker.skip_transaction_check && AfterCommitQueue.inside_transaction?
- raise Sidekiq::Worker::EnqueueFromTransactionError, <<~MSG
+ begin
+ raise Sidekiq::Worker::EnqueueFromTransactionError, <<~MSG
`#{self}.#{name}` cannot be called inside a transaction as this can lead to
race conditions when the worker runs before the transaction is committed and
tries to access a model that has not been saved yet.
Use an `after_commit` hook, or include `AfterCommitQueue` and use a `run_after_commit` block instead.
- MSG
+ MSG
+ rescue Sidekiq::Worker::EnqueueFromTransactionError => e
+ if Rails.env.production?
+ Rails.logger.error(e.message)
+
+ if Gitlab::Sentry.enabled?
+ Gitlab::Sentry.context
+ Raven.capture_exception(e)
+ end
+ else
+ raise
+ end
+ end
end
super(*args)
diff --git a/config/initializers/lograge.rb b/config/initializers/lograge.rb
index 8560d24526f..114c1cb512f 100644
--- a/config/initializers/lograge.rb
+++ b/config/initializers/lograge.rb
@@ -12,9 +12,14 @@ unless Sidekiq.server?
config.lograge.logger = ActiveSupport::Logger.new(filename)
# Add request parameters to log output
config.lograge.custom_options = lambda do |event|
+ params = event.payload[:params]
+ .except(*%w(controller action format))
+ .each_pair
+ .map { |k, v| { key: k, value: v } }
+
payload = {
time: event.time.utc.iso8601(3),
- params: event.payload[:params].except(*%w(controller action format)),
+ params: params,
remote_ip: event.payload[:remote_ip],
user_id: event.payload[:user_id],
username: event.payload[:username]
diff --git a/config/initializers/omniauth.rb b/config/initializers/omniauth.rb
index e9e1f1c4e9b..00baea08613 100644
--- a/config/initializers/omniauth.rb
+++ b/config/initializers/omniauth.rb
@@ -1,6 +1,6 @@
-if Gitlab::LDAP::Config.enabled?
+if Gitlab::Auth::LDAP::Config.enabled?
module OmniAuth::Strategies
- Gitlab::LDAP::Config.available_servers.each do |server|
+ Gitlab::Auth::LDAP::Config.available_servers.each do |server|
# do not redeclare LDAP
next if server['provider_name'] == 'ldap'
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 0f164e628f9..161fb185c9b 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -10,7 +10,7 @@ Sidekiq.configure_server do |config|
config.server_middleware do |chain|
chain.add Gitlab::SidekiqMiddleware::ArgumentsLogger if ENV['SIDEKIQ_LOG_ARGUMENTS']
- chain.add Gitlab::SidekiqMiddleware::MemoryKiller if ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS']
+ chain.add Gitlab::SidekiqMiddleware::Shutdown
chain.add Gitlab::SidekiqMiddleware::RequestStoreMiddleware unless ENV['SIDEKIQ_REQUEST_STORE'] == '0'
chain.add Gitlab::SidekiqStatus::ServerMiddleware
end
diff --git a/config/prometheus/additional_metrics.yml b/config/prometheus/additional_metrics.yml
index 601a86490d4..c4f60eb2687 100644
--- a/config/prometheus/additional_metrics.yml
+++ b/config/prometheus/additional_metrics.yml
@@ -140,20 +140,20 @@
priority: 5
metrics:
- title: "Memory Usage"
- y_label: "Memory Usage (MB)"
+ y_label: "Memory Used per Pod"
required_metrics:
- container_memory_usage_bytes
weight: 1
queries:
- - query_range: '(sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024'
+ - query_range: 'avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024'
label: Average
unit: MB
- - title: "CPU Utilization"
- y_label: "CPU Utilization (%)"
+ - title: "CPU Usage"
+ y_label: "Cores per Pod"
required_metrics:
- container_cpu_usage_seconds_total
weight: 1
queries:
- - query_range: 'sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100'
+ - query_range: 'avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name))'
label: Average
- unit: "%" \ No newline at end of file
+ unit: "cores" \ No newline at end of file
diff --git a/config/routes.rb b/config/routes.rb
index e72ea1881cd..35fd76fb119 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -43,8 +43,6 @@ 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/routes/project.rb b/config/routes/project.rb
index 8fe545b721e..34636285c51 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -55,7 +55,7 @@ constraints(ProjectUrlConstrainer.new) do
end
resource :pages, only: [:show, :destroy] do
- resources :domains, only: [:show, :new, :create, :destroy], controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
+ resources :domains, except: :index, controller: 'pages_domains', constraints: { id: %r{[^/]+} } do
member do
post :verify
end
@@ -103,6 +103,7 @@ constraints(ProjectUrlConstrainer.new) do
post :toggle_subscription
post :remove_wip
post :assign_related_issues
+ get :discussions, format: :json
post :rebase
scope constraints: { format: nil }, action: :show do
diff --git a/config/routes/repository.rb b/config/routes/repository.rb
index 9ffdebbcff1..eace3a615b4 100644
--- a/config/routes/repository.rb
+++ b/config/routes/repository.rb
@@ -49,6 +49,7 @@ scope format: false do
end
end
+ get '/branches/:state', to: 'branches#index', as: :branches_filtered, constraints: { state: /active|stale|all/ }
resources :branches, only: [:index, :new, :create, :destroy]
delete :merged_branches, controller: 'branches', action: :destroy_all_merged
resources :tags, only: [:index, :show, :new, :create, :destroy] do
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index f037e3d1221..4845dc28a4a 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -68,3 +68,4 @@
- [project_migrate_hashed_storage, 1]
- [storage_migrator, 1]
- [pages_domain_verification, 1]
+ - [plugin, 1]
diff --git a/config/webpack.config.js b/config/webpack.config.js
index 94ff39485fb..19eeb497a14 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -1,97 +1,63 @@
'use strict';
-var crypto = require('crypto');
-var fs = require('fs');
-var path = require('path');
-var glob = require('glob');
-var webpack = require('webpack');
-var StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
-var CopyWebpackPlugin = require('copy-webpack-plugin');
-var CompressionPlugin = require('compression-webpack-plugin');
-var NameAllModulesPlugin = require('name-all-modules-plugin');
-var BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
-var WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
-
-var ROOT_PATH = path.resolve(__dirname, '..');
-var IS_PRODUCTION = process.env.NODE_ENV === 'production';
-var IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
-var DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
-var DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
-var DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
-var WEBPACK_REPORT = process.env.WEBPACK_REPORT;
-var NO_COMPRESSION = process.env.NO_COMPRESSION;
-
-// generate automatic entry points
-var autoEntries = {};
-var pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
-
-// filter out entries currently imported dynamically in dispatcher.js
-var dispatcher = fs.readFileSync(path.join(ROOT_PATH, 'app/assets/javascripts/dispatcher.js')).toString();
-var dispatcherChunks = dispatcher.match(/(?!import\(')\.\/pages\/[^']+/g);
-
-function generateAutoEntries(path, prefix = '.') {
- const chunkPath = path.replace(/\/index\.js$/, '');
- if (!dispatcherChunks.includes(`${prefix}/${chunkPath}`)) {
+const crypto = require('crypto');
+const fs = require('fs');
+const path = require('path');
+const glob = require('glob');
+const webpack = require('webpack');
+const StatsWriterPlugin = require('webpack-stats-plugin').StatsWriterPlugin;
+const CopyWebpackPlugin = require('copy-webpack-plugin');
+const CompressionPlugin = require('compression-webpack-plugin');
+const NameAllModulesPlugin = require('name-all-modules-plugin');
+const BundleAnalyzerPlugin = require('webpack-bundle-analyzer').BundleAnalyzerPlugin;
+const WatchMissingNodeModulesPlugin = require('react-dev-utils/WatchMissingNodeModulesPlugin');
+
+const ROOT_PATH = path.resolve(__dirname, '..');
+const IS_PRODUCTION = process.env.NODE_ENV === 'production';
+const IS_DEV_SERVER = process.argv.join(' ').indexOf('webpack-dev-server') !== -1;
+const DEV_SERVER_HOST = process.env.DEV_SERVER_HOST || 'localhost';
+const DEV_SERVER_PORT = parseInt(process.env.DEV_SERVER_PORT, 10) || 3808;
+const DEV_SERVER_LIVERELOAD = process.env.DEV_SERVER_LIVERELOAD !== 'false';
+const WEBPACK_REPORT = process.env.WEBPACK_REPORT;
+const NO_COMPRESSION = process.env.NO_COMPRESSION;
+
+let autoEntriesCount = 0;
+let watchAutoEntries = [];
+
+function generateEntries() {
+ // generate automatic entry points
+ const autoEntries = {};
+ const pageEntries = glob.sync('pages/**/index.js', { cwd: path.join(ROOT_PATH, 'app/assets/javascripts') });
+ watchAutoEntries = [
+ path.join(ROOT_PATH, 'app/assets/javascripts/pages/'),
+ ];
+
+ function generateAutoEntries(path, prefix = '.') {
+ const chunkPath = path.replace(/\/index\.js$/, '');
const chunkName = chunkPath.replace(/\//g, '.');
autoEntries[chunkName] = `${prefix}/${path}`;
}
-}
-pageEntries.forEach(( path ) => generateAutoEntries(path));
+ pageEntries.forEach(( path ) => generateAutoEntries(path));
-// report our auto-generated bundle count
-var autoEntriesCount = Object.keys(autoEntries).length;
-console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
+ autoEntriesCount = Object.keys(autoEntries).length;
-var config = {
- // because sqljs requires fs.
- node: {
- fs: "empty"
- },
- context: path.join(ROOT_PATH, 'app/assets/javascripts'),
- entry: {
- balsamiq_viewer: './blob/balsamiq_viewer.js',
- blob: './blob_edit/blob_bundle.js',
- boards: './boards/boards_bundle.js',
+ const manualEntries = {
common: './commons/index.js',
common_vue: './vue_shared/vue_resource_interceptor.js',
- cycle_analytics: './cycle_analytics/cycle_analytics_bundle.js',
- commit_pipelines: './commit/pipelines/pipelines_bundle.js',
- deploy_keys: './deploy_keys/index.js',
- diff_notes: './diff_notes/diff_notes_bundle.js',
- environments: './environments/environments_bundle.js',
- environments_folder: './environments/folder/environments_folder_bundle.js',
- filtered_search: './filtered_search/filtered_search_bundle.js',
- help: './help/help.js',
- issue_show: './issue_show/index.js',
locale: './locale/index.js',
main: './main.js',
- merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
- monitoring: './monitoring/monitoring_bundle.js',
- network: './network/network_bundle.js',
- notebook_viewer: './blob/notebook_viewer.js',
- pdf_viewer: './blob/pdf_viewer.js',
- pipelines: './pipelines/pipelines_bundle.js',
- pipelines_details: './pipelines/pipeline_details_bundle.js',
- profile: './profile/profile_bundle.js',
- project_import_gl: './projects/project_import_gitlab_project.js',
- protected_branches: './protected_branches',
- protected_tags: './protected_tags',
- registry_list: './registry/index.js',
- ide: './ide/index.js',
- sidebar: './sidebar/sidebar_bundle.js',
- snippet: './snippet/snippet_bundle.js',
- sketch_viewer: './blob/sketch_viewer.js',
- stl_viewer: './blob/stl_viewer.js',
- terminal: './terminal/terminal_bundle.js',
- u2f: ['vendor/u2f'],
- ui_development_kit: './ui_development_kit.js',
raven: './raven/index.js',
- vue_merge_request_widget: './vue_merge_request_widget/index.js',
- test: './test.js',
- two_factor_auth: './two_factor_auth.js',
webpack_runtime: './webpack.js',
- },
+ };
+
+ return Object.assign(manualEntries, autoEntries);
+}
+
+const config = {
+ context: path.join(ROOT_PATH, 'app/assets/javascripts'),
+
+ entry: generateEntries,
output: {
path: path.join(ROOT_PATH, 'public/assets/webpack'),
@@ -180,7 +146,7 @@ var config = {
new StatsWriterPlugin({
filename: 'manifest.json',
transform: function(data, opts) {
- var stats = opts.compiler.getStats().toJson({
+ const stats = opts.compiler.getStats().toJson({
chunkModules: false,
source: false,
chunks: false,
@@ -239,38 +205,6 @@ var config = {
return `${moduleNames[0]}-${hash.substr(0, 6)}`;
}),
- // create cacheable common library bundle for all vue chunks
- new webpack.optimize.CommonsChunkPlugin({
- name: 'common_vue',
- chunks: [
- 'boards',
- 'commit_pipelines',
- 'cycle_analytics',
- 'deploy_keys',
- 'diff_notes',
- 'environments',
- 'environments_folder',
- 'filtered_search',
- 'groups',
- 'issue_show',
- 'merge_conflicts',
- 'monitoring',
- 'notebook_viewer',
- 'pdf_viewer',
- 'pipelines',
- 'pipelines_details',
- 'registry_list',
- 'ide',
- 'schedule_form',
- 'schedules_index',
- 'sidebar',
- 'vue_merge_request_widget',
- ],
- minChunks: function(module, count) {
- return module.resource && (/vue_shared/).test(module.resource);
- },
- }),
-
// create cacheable common library bundles
new webpack.optimize.CommonsChunkPlugin({
names: ['main', 'common', 'webpack_runtime'],
@@ -310,11 +244,15 @@ var config = {
'images': path.join(ROOT_PATH, 'app/assets/images'),
'vendor': path.join(ROOT_PATH, 'vendor/assets/javascripts'),
'vue$': 'vue/dist/vue.esm.js',
+ 'spec': path.join(ROOT_PATH, 'spec/javascripts'),
}
- }
-}
+ },
-config.entry = Object.assign({}, autoEntries, config.entry);
+ // sqljs requires fs
+ node: {
+ fs: 'empty',
+ },
+};
if (IS_PRODUCTION) {
config.devtool = 'source-map';
@@ -351,7 +289,24 @@ if (IS_DEV_SERVER) {
};
config.plugins.push(
// watch node_modules for changes if we encounter a missing module compile error
- new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules'))
+ new WatchMissingNodeModulesPlugin(path.join(ROOT_PATH, 'node_modules')),
+
+ // watch for changes to our automatic entry point modules
+ {
+ apply(compiler) {
+ compiler.plugin('emit', (compilation, callback) => {
+ compilation.contextDependencies = [
+ ...compilation.contextDependencies,
+ ...watchAutoEntries,
+ ];
+
+ // report our auto-generated bundle count
+ console.log(`${autoEntriesCount} entries from '/pages' automatically added to webpack output.`);
+
+ callback();
+ })
+ },
+ }
);
if (DEV_SERVER_LIVERELOAD) {
config.plugins.push(new webpack.HotModuleReplacementPlugin());
diff --git a/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb
new file mode 100644
index 00000000000..dbe09a43aa7
--- /dev/null
+++ b/db/migrate/20180212030105_add_external_ip_to_clusters_applications_ingress.rb
@@ -0,0 +1,9 @@
+class AddExternalIpToClustersApplicationsIngress < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :clusters_applications_ingress, :external_ip, :string
+ end
+end
diff --git a/db/migrate/20180214093516_create_badges.rb b/db/migrate/20180214093516_create_badges.rb
new file mode 100644
index 00000000000..6559f834484
--- /dev/null
+++ b/db/migrate/20180214093516_create_badges.rb
@@ -0,0 +1,17 @@
+class CreateBadges < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ create_table :badges do |t|
+ t.string :link_url, null: false
+ t.string :image_url, null: false
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: true
+ t.integer :group_id, index: true, null: true
+ t.string :type, null: false
+
+ t.timestamps_with_timezone null: false
+ end
+
+ add_foreign_key :badges, :namespaces, column: :group_id, on_delete: :cascade
+ end
+end
diff --git a/db/migrate/20180214155405_create_clusters_applications_runners.rb b/db/migrate/20180214155405_create_clusters_applications_runners.rb
new file mode 100644
index 00000000000..fc4c0881338
--- /dev/null
+++ b/db/migrate/20180214155405_create_clusters_applications_runners.rb
@@ -0,0 +1,32 @@
+class CreateClustersApplicationsRunners < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ create_table :clusters_applications_runners do |t|
+ t.references :cluster, null: false, foreign_key: { on_delete: :cascade }
+ t.references :runner, references: :ci_runners
+ t.index :runner_id
+ t.index :cluster_id, unique: true
+ t.integer :status, null: false
+ t.timestamps_with_timezone null: false
+ t.string :version, null: false
+ t.text :status_reason
+ end
+
+ add_concurrent_foreign_key :clusters_applications_runners, :ci_runners,
+ column: :runner_id,
+ on_delete: :nullify
+ end
+
+ def down
+ if foreign_keys_for(:clusters_applications_runners, :runner_id).any?
+ remove_foreign_key :clusters_applications_runners, column: :runner_id
+ end
+
+ drop_table :clusters_applications_runners
+ end
+end
diff --git a/db/migrate/20180222043024_add_ip_address_to_runner.rb b/db/migrate/20180222043024_add_ip_address_to_runner.rb
new file mode 100644
index 00000000000..bf00560b5a8
--- /dev/null
+++ b/db/migrate/20180222043024_add_ip_address_to_runner.rb
@@ -0,0 +1,9 @@
+class AddIpAddressToRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :ci_runners, :ip_address, :string
+ end
+end
diff --git a/db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb b/db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb
new file mode 100644
index 00000000000..54e6e35449e
--- /dev/null
+++ b/db/migrate/20180226050030_add_checksum_to_ci_job_artifacts.rb
@@ -0,0 +1,7 @@
+class AddChecksumToCiJobArtifacts < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :ci_job_artifacts, :file_sha256, :binary
+ end
+end
diff --git a/db/migrate/20180304204842_clean_commits_count_migration.rb b/db/migrate/20180304204842_clean_commits_count_migration.rb
new file mode 100644
index 00000000000..ace4c6aa1cf
--- /dev/null
+++ b/db/migrate/20180304204842_clean_commits_count_migration.rb
@@ -0,0 +1,14 @@
+class CleanCommitsCountMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Gitlab::BackgroundMigration.steal('AddMergeRequestDiffCommitsCount')
+ end
+
+ def down
+ end
+end
diff --git a/db/migrate/20180305144721_add_privileged_to_runner.rb b/db/migrate/20180305144721_add_privileged_to_runner.rb
new file mode 100644
index 00000000000..32e73dba8d5
--- /dev/null
+++ b/db/migrate/20180305144721_add_privileged_to_runner.rb
@@ -0,0 +1,18 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddPrivilegedToRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :clusters_applications_runners, :privileged, :boolean, default: true, allow_null: false
+ end
+
+ def down
+ remove_column :clusters_applications_runners, :privileged
+ end
+end
diff --git a/db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb b/db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb
new file mode 100644
index 00000000000..e55e2e6f888
--- /dev/null
+++ b/db/post_migrate/20180212101828_add_tmp_partial_null_index_to_builds.rb
@@ -0,0 +1,14 @@
+class AddTmpPartialNullIndexToBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
+ name: 'tmp_id_partial_null_index')
+ end
+
+ def down
+ remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
+ end
+end
diff --git a/db/post_migrate/20180212101928_schedule_build_stage_migration.rb b/db/post_migrate/20180212101928_schedule_build_stage_migration.rb
new file mode 100644
index 00000000000..df15b2cd9d4
--- /dev/null
+++ b/db/post_migrate/20180212101928_schedule_build_stage_migration.rb
@@ -0,0 +1,29 @@
+class ScheduleBuildStageMigration < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ MIGRATION = 'MigrateBuildStage'.freeze
+ BATCH_SIZE = 500
+
+ disable_ddl_transaction!
+
+ class Build < ActiveRecord::Base
+ include EachBatch
+ self.table_name = 'ci_builds'
+ end
+
+ def up
+ disable_statement_timeout
+
+ Build.where('stage_id IS NULL').tap do |relation|
+ queue_background_migration_jobs_by_range_at_intervals(relation,
+ MIGRATION,
+ 5.minutes,
+ batch_size: BATCH_SIZE)
+ end
+ end
+
+ def down
+ # noop
+ end
+end
diff --git a/db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb b/db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb
new file mode 100644
index 00000000000..ed7b1fc72f4
--- /dev/null
+++ b/db/post_migrate/20180212102028_remove_tmp_partial_null_index_from_builds.rb
@@ -0,0 +1,14 @@
+class RemoveTmpPartialNullIndexFromBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ remove_concurrent_index_by_name(:ci_builds, 'tmp_id_partial_null_index')
+ end
+
+ def down
+ add_concurrent_index(:ci_builds, :id, where: 'stage_id IS NULL',
+ name: 'tmp_id_partial_null_index')
+ end
+end
diff --git a/db/post_migrate/20180301084653_change_project_namespace_id_not_null.rb b/db/post_migrate/20180301084653_change_project_namespace_id_not_null.rb
new file mode 100644
index 00000000000..0342372cbed
--- /dev/null
+++ b/db/post_migrate/20180301084653_change_project_namespace_id_not_null.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 ChangeProjectNamespaceIdNotNull < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ class Project < ActiveRecord::Base
+ self.table_name = 'projects'
+ include EachBatch
+ end
+
+ BATCH_SIZE = 1000
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ Project.where(namespace_id: nil).each_batch(of: BATCH_SIZE) do |batch|
+ batch.delete_all
+ end
+
+ change_column_null :projects, :namespace_id, false
+ end
+
+ def down
+ change_column_null :projects, :namespace_id, true
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5bb461169f1..0881a1af945 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: 20180216121030) do
+ActiveRecord::Schema.define(version: 20180305144721) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -183,6 +183,19 @@ ActiveRecord::Schema.define(version: 20180216121030) do
add_index "award_emoji", ["awardable_type", "awardable_id"], name: "index_award_emoji_on_awardable_type_and_awardable_id", using: :btree
add_index "award_emoji", ["user_id", "name"], name: "index_award_emoji_on_user_id_and_name", using: :btree
+ create_table "badges", force: :cascade do |t|
+ t.string "link_url", null: false
+ t.string "image_url", null: false
+ t.integer "project_id"
+ t.integer "group_id"
+ t.string "type", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ end
+
+ add_index "badges", ["group_id"], name: "index_badges_on_group_id", using: :btree
+ add_index "badges", ["project_id"], name: "index_badges_on_project_id", using: :btree
+
create_table "boards", force: :cascade do |t|
t.integer "project_id", null: false
t.datetime "created_at", null: false
@@ -333,6 +346,7 @@ ActiveRecord::Schema.define(version: 20180216121030) do
t.datetime_with_timezone "updated_at", null: false
t.datetime_with_timezone "expire_at"
t.string "file"
+ t.binary "file_sha256"
end
add_index "ci_job_artifacts", ["expire_at", "job_id"], name: "index_ci_job_artifacts_on_expire_at_and_job_id", using: :btree
@@ -437,6 +451,7 @@ ActiveRecord::Schema.define(version: 20180216121030) do
t.boolean "run_untagged", default: true, null: false
t.boolean "locked", default: false, null: false
t.integer "access_level", default: 0, null: false
+ t.string "ip_address"
end
add_index "ci_runners", ["contacted_at"], name: "index_ci_runners_on_contacted_at", using: :btree
@@ -569,6 +584,7 @@ ActiveRecord::Schema.define(version: 20180216121030) do
t.string "version", null: false
t.string "cluster_ip"
t.text "status_reason"
+ t.string "external_ip"
end
create_table "clusters_applications_prometheus", force: :cascade do |t|
@@ -580,6 +596,20 @@ ActiveRecord::Schema.define(version: 20180216121030) do
t.datetime_with_timezone "updated_at", null: false
end
+ create_table "clusters_applications_runners", force: :cascade do |t|
+ t.integer "cluster_id", null: false
+ t.integer "runner_id"
+ t.integer "status", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.string "version", null: false
+ t.text "status_reason"
+ t.boolean "privileged", default: true, null: false
+ end
+
+ add_index "clusters_applications_runners", ["cluster_id"], name: "index_clusters_applications_runners_on_cluster_id", unique: true, using: :btree
+ add_index "clusters_applications_runners", ["runner_id"], name: "index_clusters_applications_runners_on_runner_id", using: :btree
+
create_table "container_repositories", force: :cascade do |t|
t.integer "project_id", null: false
t.string "name", null: false
@@ -1426,7 +1456,7 @@ ActiveRecord::Schema.define(version: 20180216121030) do
t.datetime "created_at"
t.datetime "updated_at"
t.integer "creator_id"
- t.integer "namespace_id"
+ t.integer "namespace_id", null: false
t.datetime "last_activity_at"
t.string "import_url"
t.integer "visibility_level", default: 0, null: false
@@ -1954,6 +1984,8 @@ ActiveRecord::Schema.define(version: 20180216121030) do
add_index "web_hooks", ["project_id"], name: "index_web_hooks_on_project_id", using: :btree
add_index "web_hooks", ["type"], name: "index_web_hooks_on_type", using: :btree
+ add_foreign_key "badges", "namespaces", column: "group_id", on_delete: :cascade
+ add_foreign_key "badges", "projects", on_delete: :cascade
add_foreign_key "boards", "projects", name: "fk_f15266b5f9", on_delete: :cascade
add_foreign_key "chat_teams", "namespaces", on_delete: :cascade
add_foreign_key "ci_build_trace_section_names", "projects", on_delete: :cascade
@@ -1986,6 +2018,8 @@ ActiveRecord::Schema.define(version: 20180216121030) do
add_foreign_key "cluster_providers_gcp", "clusters", on_delete: :cascade
add_foreign_key "clusters", "users", on_delete: :nullify
add_foreign_key "clusters_applications_helm", "clusters", on_delete: :cascade
+ add_foreign_key "clusters_applications_runners", "ci_runners", column: "runner_id", name: "fk_02de2ded36", on_delete: :nullify
+ add_foreign_key "clusters_applications_runners", "clusters", on_delete: :cascade
add_foreign_key "container_repositories", "projects"
add_foreign_key "deploy_keys_projects", "projects", name: "fk_58a901ca7e", on_delete: :cascade
add_foreign_key "deployments", "projects", name: "fk_b9a3851b82", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index 46fcb7c6baf..fb7a23e2750 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -1,5 +1,4 @@
---
-toc: false
comments: false
---
@@ -8,15 +7,9 @@ comments: false
Welcome to [GitLab](https://about.gitlab.com/), a Git-based fully featured
platform for software development!
-GitLab offers the most scalable Git-based fully integrated platform for software development, with flexible products and subscription plans.
-
-With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
-
-Every feature available in Libre is also available in Starter, Premium, and Ultimate.
-Starter features are also available in Premium and Ultimate, and Premium features are also
-available in Ultimate.
-
-GitLab.com is our SaaS offering. It's hosted, managed, and administered by GitLab, with [free and paid plans](https://about.gitlab.com/gitlab-com/) for individuals and teams: Free, Bronze, Silver, and Gold.
+GitLab offers the most scalable Git-based fully integrated platform for
+software development, with flexible products and subscriptions.
+To understand what features you have access to, check the [GitLab subscriptions](#gitlab-subscriptions) below.
## Shortcuts to GitLab's most visited docs
@@ -124,8 +117,6 @@ Manage your [repositories](user/project/repository/index.md) from the UI (user i
- [GitLab Integration](integration/README.md): Integrate with multiple third-party services with GitLab to allow external issue trackers and external authentication.
- [Trello Power-Up](integration/trello_power_up.md): Integrate with GitLab's Trello Power-Up
-----
-
## Administrator documentation
[Administration documentation](administration/index.md) applies to admin users of GitLab
@@ -143,3 +134,42 @@ Learn how to contribute to GitLab:
- [Development](development/README.md): All styleguides and explanations how to contribute.
- [Legal](legal/README.md): Contributor license agreements.
- [Writing documentation](development/writing_documentation.md): Contributing to GitLab Docs.
+
+## GitLab subscriptions
+
+You have two options to use GitLab:
+
+- GitLab self-hosted: Install, administer, and maintain your own GitLab instance.
+- GitLab.com: GitLab's SaaS offering. You don't need to install anything to use GitLab.com,
+you only need to [sign up](https://gitlab.com/users/sign_in) and start using GitLab
+straight away.
+
+### GitLab self-hosted
+
+With GitLab self-hosted, you deploy your own GitLab instance on-premises or on a private cloud of your choice. GitLab self-hosted is available for [free and with paid subscriptions](https://about.gitlab.com/products/): Libre, Starter, Premium, and Ultimate.
+
+Every feature available in Libre is also available in Starter, Premium, and Ultimate.
+Starter features are also available in Premium and Ultimate, and Premium features are also
+available in Ultimate.
+
+### GitLab.com
+
+GitLab.com is hosted, managed, and administered by GitLab, Inc., with
+[free and paid subscriptions](https://about.gitlab.com/gitlab-com/) for individuals
+and teams: Free, Bronze, Silver, and Gold.
+
+GitLab.com subscriptions grants access
+to the same features available in GitLab self-hosted, **expect
+[administration](administration/index.md) tools and settings**:
+
+- GitLab.com Free includes the same features available in GitLab Libre
+- GitLab.com Bronze includes the same features available in GitLab Starter
+- GitLab.com Silver includes the same features available in GitLab Premium
+- GitLab.com Gold includes the same features available in GitLab Ultimate
+
+For supporting the open source community and encouraging the development of
+open source projects, GitLab grants access to **Gold** features
+for all GitLab.com **public** projects, regardless of the subscription.
+
+To know more about GitLab subscriptions and licensing, please refer to the
+[GitLab Product Marketing Handbook](https://about.gitlab.com/handbook/marketing/product-marketing/#tiers).
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index e3b10119090..d9a61aea6ef 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -2,7 +2,9 @@
[Gitaly](https://gitlab.com/gitlab-org/gitaly) (introduced in GitLab
9.0) is a service that provides high-level RPC access to Git
-repositories. Gitaly is a mandatory component in GitLab 9.4 and newer.
+repositories. Gitaly was optional when it was first introduced in
+GitLab, but since GitLab 9.4 it is a mandatory component of the
+application.
GitLab components that access Git repositories (gitlab-rails,
gitlab-shell, gitlab-workhorse) act as clients to Gitaly. End users do
@@ -184,14 +186,20 @@ Gitaly logs on your Gitaly server (`sudo gitlab-ctl tail gitaly` or
coming in. One sure way to trigger a Gitaly request is to clone a
repository from your GitLab server over HTTP.
-## Disabling or enabling the Gitaly service
+## Disabling or enabling the Gitaly service in a cluster environment
If you are running Gitaly [as a remote
service](#running-gitaly-on-its-own-server) you may want to disable
the local Gitaly service that runs on your Gitlab server by default.
-To disable the Gitaly service in your Omnibus installation, add the
-following line to `/etc/gitlab/gitlab.rb`:
+> 'Disabling Gitaly' only makes sense when you run GitLab in a custom
+cluster configuration, where different services run on different
+machines. Disabling Gitaly on all machines in the cluster is not a
+valid configuration.
+
+If you are setting up a GitLab cluster where Gitaly does not need to
+run on all machines, you can disable the Gitaly service in your
+Omnibus installation, add the following line to `/etc/gitlab/gitlab.rb`:
```ruby
gitaly['enable'] = false
@@ -200,11 +208,13 @@ gitaly['enable'] = false
When you run `gitlab-ctl reconfigure` the Gitaly service will be
disabled.
-To disable the Gitaly service in an installation from source, add the
-following to `/etc/default/gitlab`:
+To disable the Gitaly service in a GitLab cluster where you installed
+GitLab from source, add the following to `/etc/default/gitlab` on the
+machine where you want to disable Gitaly.
```shell
gitaly_enabled=false
```
-When you run `service gitlab restart` Gitaly will be disabled.
+When you run `service gitlab restart` Gitaly will be disabled on this
+particular machine.
diff --git a/doc/administration/incoming_email.md b/doc/administration/incoming_email.md
new file mode 100644
index 00000000000..6c5a466ced5
--- /dev/null
+++ b/doc/administration/incoming_email.md
@@ -0,0 +1,331 @@
+# Incoming email
+
+GitLab has several features based on receiving incoming emails:
+
+- [Reply by Email](reply_by_email.md): allow GitLab users to comment on issues
+ and merge requests by replying to notification emails.
+- [New issue by email](../user/project/issues/create_new_issue.md#new-issue-via-email):
+ allow GitLab users to create a new issue by sending an email to a
+ user-specific email address.
+- [New merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email):
+ allow GitLab users to create a new merge request by sending an email to a
+ user-specific email address.
+
+## Requirements
+
+Handling incoming emails requires an [IMAP]-enabled email account. GitLab
+requires one of the following three strategies:
+
+- Email sub-addressing
+- Dedicated email address
+- Catch-all mailbox
+
+Let's walk through each of these options.
+
+**If your provider or server supports email sub-addressing, we recommend using it.
+Most features (other than reply by email) only work with sub-addressing.**
+
+[IMAP]: https://en.wikipedia.org/wiki/Internet_Message_Access_Protocol
+
+### Email sub-addressing
+
+[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
+a feature where any email to `user+some_arbitrary_tag@example.com` will end up
+in the mailbox for `user@example.com`, and is supported by providers such as
+Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the
+[Postfix mail server] which you can run on-premises.
+
+[Postfix mail server]: reply_by_email_postfix_setup.md
+
+### Dedicated email address
+
+This solution is really simple to set up: you just have to create an email
+address dedicated to receive your users' replies to GitLab notifications.
+
+### Catch-all mailbox
+
+A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
+"catch all" the emails addressed to the domain that do not exist in the mail
+server.
+
+GitLab can be set up to allow users to comment on issues and merge requests by
+replying to notification emails.
+
+## Set it up
+
+If you want to use Gmail / Google Apps for incoming emails, make sure you have
+[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
+and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
+or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
+and use [an application password](https://support.google.com/mail/answer/185833).
+
+To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
+[Postfix setup documentation](reply_by_email_postfix_setup.md).
+
+### Security Concerns
+
+**WARNING:** Be careful when choosing the domain used for receiving incoming
+email.
+
+For the sake of example, suppose your top-level company domain is `hooli.com`.
+All employees in your company have an email address at that domain via Google
+Apps, and your company's private Slack instance requires a valid `@hooli.com`
+email address in order to sign up.
+
+If you also host a public-facing GitLab instance at `hooli.com` and set your
+incoming email domain to `hooli.com`, an attacker could abuse the "Create new
+issue by email" or
+"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)"
+features by using a project's unique address as the email when signing up for
+Slack, which would send a confirmation email, which would create a new issue or
+merge request on the project owned by the attacker, allowing them to click the
+confirmation link and validate their account on your company's private Slack
+instance.
+
+We recommend receiving incoming email on a subdomain, such as
+`incoming.hooli.com`, and ensuring that you do not employ any services that
+authenticate solely based on access to an email domain such as `*.hooli.com.`
+Alternatively, use a dedicated domain for GitLab email communications such as
+`hooli-gitlab.com`.
+
+See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366)
+for a real-world example of this exploit.
+
+### Omnibus package installations
+
+1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
+ feature and fill in the details for your specific IMAP server and email account:
+
+ Configuration for Postfix mail server, assumes mailbox
+ incoming@gitlab.example.com
+
+ ```ruby
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "incoming"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "gitlab.example.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 143
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = false
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ # The IDLE command timeout.
+ gitlab_rails['incoming_email_idle_timeout'] = 60
+ ```
+
+ Configuration for Gmail / Google Apps, assumes mailbox
+ gitlab-incoming@gmail.com
+
+ ```ruby
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "imap.gmail.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 993
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = true
+ # Whether the IMAP server uses StartTLS
+ gitlab_rails['incoming_email_start_tls'] = false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ gitlab_rails['incoming_email_mailbox_name'] = "inbox"
+ # The IDLE command timeout.
+ gitlab_rails['incoming_email_idle_timeout'] = 60
+ ```
+
+ Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes
+ mailbox incoming@exchange.example.com
+
+ ```ruby
+ gitlab_rails['incoming_email_enabled'] = true
+
+ # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
+ gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com"
+
+ # Email account username
+ # Typically this is the userPrincipalName (UPN)
+ gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com"
+ # Email account password
+ gitlab_rails['incoming_email_password'] = "[REDACTED]"
+
+ # IMAP server host
+ gitlab_rails['incoming_email_host'] = "exchange.example.com"
+ # IMAP server port
+ gitlab_rails['incoming_email_port'] = 993
+ # Whether the IMAP server uses SSL
+ gitlab_rails['incoming_email_ssl'] = true
+ ```
+
+1. Reconfigure GitLab for the changes to take effect:
+
+ ```sh
+ sudo gitlab-ctl reconfigure
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo gitlab-rake gitlab:incoming_email:check
+ ```
+
+1. Reply by email should now be working.
+
+### Installations from source
+
+1. Go to the GitLab installation directory:
+
+ ```sh
+ cd /home/git/gitlab
+ ```
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
+ and fill in the details for your specific IMAP server and email account:
+
+ ```sh
+ sudo editor config/gitlab.yml
+ ```
+
+ Configuration for Postfix mail server, assumes mailbox
+ incoming@gitlab.example.com
+
+ ```yaml
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "incoming+%{key}@gitlab.example.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "incoming"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "gitlab.example.com"
+ # IMAP server port
+ port: 143
+ # Whether the IMAP server uses SSL
+ ssl: false
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
+ ```
+
+ Configuration for Gmail / Google Apps, assumes mailbox
+ gitlab-incoming@gmail.com
+
+ ```yaml
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
+ ```
+
+ Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes
+ mailbox incoming@exchange.example.com
+
+ ```yaml
+ incoming_email:
+ enabled: true
+
+ # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
+ address: "incoming@exchange.example.com"
+
+ # Email account username
+ # Typically this is the userPrincipalName (UPN)
+ user: "incoming@ad-domain.example.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "exchange.example.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
+ ```
+
+1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
+
+ ```sh
+ sudo mkdir -p /etc/default
+ echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
+ ```
+
+1. Restart GitLab:
+
+ ```sh
+ sudo service gitlab restart
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
+ ```
+
+1. Reply by email should now be working.
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 51444651bdb..69efaf75140 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -79,11 +79,19 @@ created in snippets, wikis, and repos.
- [Sign-up restrictions](../user/admin_area/settings/sign_up_restrictions.md): block email addresses of specific domains, or whitelist only specific domains.
- [Access restrictions](../user/admin_area/settings/visibility_and_access_controls.md#enabled-git-access-protocols): Define which Git access protocols can be used to talk to GitLab (SSH, HTTP, HTTPS).
- [Authentication/Authorization](../topics/authentication/index.md#gitlab-administrators): Enforce 2FA, configure external authentication with LDAP, SAML, CAS and additional Omniauth providers.
-- [Reply by email](reply_by_email.md): Allow users to comment on issues and merge requests by replying to notification emails.
- - [Postfix for Reply by email](reply_by_email_postfix_setup.md): Set up a basic Postfix mail
+- [Incoming email](incoming_email.md): Configure incoming emails to allow
+ users to [reply by email], create [issues by email] and
+ [merge requests by email], and to enable [Service Desk].
+ - [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
+ basic Postfix mail server with IMAP authentication on Ubuntu for incoming
+ emails.
server with IMAP authentication on Ubuntu, to be used with Reply by email.
- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
+[reply by email]: reply_by_email.md
+[issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email
+[merge requests by email]: ../user/project/merge_requests/index.md#create-new-merge-requests-by-email
+
## Project settings
- [Container Registry](container_registry.md): Configure Container Registry with GitLab.
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 1b42d7979ed..00a2f3d01b8 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -23,7 +23,7 @@ requests from the API are logged to a separate file in `api_json.log`.
Each line contains a JSON line that can be ingested by Elasticsearch, Splunk, etc. For example:
```json
-{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":{"namespace_id":"gitlab","project_id":"gitlab-ce","id":"1234"},"remote_ip":"18.245.0.1","user_id":1,"username":"admin"}
+{"method":"GET","path":"/gitlab/gitlab-ce/issues/1234","format":"html","controller":"Projects::IssuesController","action":"show","status":200,"duration":229.03,"view":174.07,"db":13.24,"time":"2017-08-08T20:15:54.821Z","params":[{"key":"param_key","value":"param_value"}],"remote_ip":"18.245.0.1","user_id":1,"username":"admin","gitaly_calls":76}
```
In this example, you can see this was a GET request for a specific issue. Notice each line also contains performance data:
@@ -31,6 +31,7 @@ In this example, you can see this was a GET request for a specific issue. Notice
1. `duration`: the total time taken to retrieve the request
2. `view`: total time taken inside the Rails views
3. `db`: total time to retrieve data from the database
+4. `gitaly_calls`: total number of calls made to Gitaly
User clone/fetch activity using http transport appears in this log as `action: git_upload_pack`.
diff --git a/doc/administration/plugins.md b/doc/administration/plugins.md
new file mode 100644
index 00000000000..c91ac3012b9
--- /dev/null
+++ b/doc/administration/plugins.md
@@ -0,0 +1,66 @@
+# Plugins
+
+**Note:** Plugins must be configured on the filesystem of the GitLab
+server. Only GitLab server administrators will be able to complete these tasks.
+Please explore [system hooks] or [webhooks] as an option if you do not
+have filesystem access.
+
+Introduced in GitLab 10.6.
+
+A plugin will run on each event so it's up to you to filter events or projects within a plugin code. You can have as many plugins as you want. Each plugin will be triggered by GitLab asynchronously in case of an event. For a list of events please see [system hooks] documentation.
+
+## Setup
+
+Plugins must be placed directly into `plugins` directory, subdirectories will be ignored.
+There is an `example` directory inside `plugins` where you can find some basic examples.
+
+Follow the steps below to set up a custom hook:
+
+1. On the GitLab server, navigate to the project's plugin directory.
+ For an installation from source the path is usually
+ `/home/git/gitlab/plugins/`. For Omnibus installs the path is
+ usually `/opt/gitlab/embedded/service/gitlab-rails/plugins`.
+1. Inside the `plugins` directory, create a file with a name of your choice, but without spaces or special characters.
+1. Make the hook file executable and make sure it's owned by the git user.
+1. Write the code to make the plugin function as expected. Plugin can be
+ in any language. Ensure the 'shebang' at the top properly reflects the language
+ type. For example, if the script is in Ruby the shebang will probably be
+ `#!/usr/bin/env ruby`.
+1. The data to the plugin will be provided as JSON on STDIN. It will be exactly same as one for [system hooks]
+
+That's it! Assuming the plugin code is properly implemented the hook will fire
+as appropriate. Plugins file list is updated for each event. There is no need to restart GitLab to apply a new plugin.
+
+If a plugin executes with non-zero exit code or GitLab fails to execute it, a
+message will be logged to `plugin.log`.
+
+## Validation
+
+Writing own plugin can be tricky and its easier if you can check it without altering the system.
+We provided a rake task you can use with staging environment to test your plugin before using it in production.
+The rake task will use a sample data and execute each of plugins. By output you should be able to determine if
+system sees your plugin and if it was executed without errors.
+
+```bash
+# Omnibus installations
+sudo gitlab-rake plugins:validate
+
+# Installations from source
+bundle exec rake plugins:validate RAILS_ENV=production
+```
+
+Example of output can be next:
+
+```
+-> bundle exec rake plugins:validate RAILS_ENV=production
+Validating plugins from /plugins directory
+* /home/git/gitlab/plugins/save_to_file.clj succeed (zero exit code)
+* /home/git/gitlab/plugins/save_to_file.rb failure (non-zero exit code)
+```
+
+[hooks]: https://git-scm.com/book/en/v2/Customizing-Git-Git-Hooks#Server-Side-Hooks
+[system hooks]: ../system_hooks/system_hooks.md
+[webhooks]: ../user/project/integrations/webhooks.md
+[5073]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5073
+[93]: https://gitlab.com/gitlab-org/gitlab-shell/merge_requests/93
+
diff --git a/doc/administration/raketasks/check.md b/doc/administration/raketasks/check.md
index d1ed152b58c..d73d9422d2c 100644
--- a/doc/administration/raketasks/check.md
+++ b/doc/administration/raketasks/check.md
@@ -78,34 +78,41 @@ Example output:
## Uploaded Files Integrity
-The uploads check Rake task will loop through all uploads in the database
-and run two checks to determine the integrity of each file:
+Various types of file can be uploaded to a GitLab installation by users.
+Checksums are generated and stored in the database upon upload, and integrity
+checks using those checksums can be run. These checks also detect missing files.
-1. Check if the file exist on the file system.
-1. Check if the checksum of the file on the file system matches the checksum in the database.
+Currently, integrity checks are supported for the following types of file:
+
+* LFS objects
+* User uploads
**Omnibus Installation**
```
+sudo gitlab-rake gitlab:lfs:check
sudo gitlab-rake gitlab:uploads:check
```
**Source Installation**
```bash
+sudo -u git -H bundle exec rake gitlab:lfs:check RAILS_ENV=production
sudo -u git -H bundle exec rake gitlab:uploads:check RAILS_ENV=production
```
-This task also accepts some environment variables which you can use to override
+These tasks also accept some environment variables which you can use to override
certain values:
-Variable | Type | Description
--------- | ---- | -----------
-`BATCH` | integer | Specifies the size of the batch. Defaults to 200.
-`ID_FROM` | integer | Specifies the ID to start from, inclusive of the value.
-`ID_TO` | integer | Specifies the ID value to end at, inclusive of the value.
+Variable | Type | Description
+--------- | ------- | -----------
+`BATCH` | integer | Specifies the size of the batch. Defaults to 200.
+`ID_FROM` | integer | Specifies the ID to start from, inclusive of the value.
+`ID_TO` | integer | Specifies the ID value to end at, inclusive of the value.
+`VERBOSE` | boolean | Causes failures to be listed individually, rather than being summarized.
```bash
+sudo gitlab-rake gitlab:lfs:check BATCH=100 ID_FROM=50 ID_TO=250
sudo gitlab-rake gitlab:uploads:check BATCH=100 ID_FROM=50 ID_TO=250
```
diff --git a/doc/administration/reply_by_email.md b/doc/administration/reply_by_email.md
index 3a2cced37bf..426245c7aca 100644
--- a/doc/administration/reply_by_email.md
+++ b/doc/administration/reply_by_email.md
@@ -5,33 +5,7 @@ replying to notification emails.
## Requirement
-Reply by email requires an IMAP-enabled email account. GitLab allows you to use
-three strategies for this feature:
-- using email sub-addressing
-- using a dedicated email address
-- using a catch-all mailbox
-
-### Email sub-addressing
-
-**If your provider or server supports email sub-addressing, we recommend using it.
-Some features (e.g. create new issue via email) only work with sub-addressing.**
-
-[Sub-addressing](https://en.wikipedia.org/wiki/Email_address#Sub-addressing) is
-a feature where any email to `user+some_arbitrary_tag@example.com` will end up
-in the mailbox for `user@example.com`, and is supported by providers such as
-Gmail, Google Apps, Yahoo! Mail, Outlook.com and iCloud, as well as the Postfix
-mail server which you can run on-premises.
-
-### Dedicated email address
-
-This solution is really simple to set up: you just have to create an email
-address dedicated to receive your users' replies to GitLab notifications.
-
-### Catch-all mailbox
-
-A [catch-all mailbox](https://en.wikipedia.org/wiki/Catch-all) for a domain will
-"catch all" the emails addressed to the domain that do not exist in the mail
-server.
+Make sure [incoming email](incoming_email.md) is setup.
## How it works?
@@ -65,329 +39,3 @@ the entity the notification was about (issue, merge request, commit...).
For more details about the `Message-ID`, `In-Reply-To`, and `References headers`,
please consult [RFC 5322](https://tools.ietf.org/html/rfc5322#section-3.6.4).
-
-## Set it up
-
-If you want to use Gmail / Google Apps with Reply by email, make sure you have
-[IMAP access enabled](https://support.google.com/mail/troubleshooter/1668960?hl=en#ts=1665018)
-and [allowed less secure apps to access the account](https://support.google.com/accounts/answer/6010255)
-or [turn-on 2-step validation](https://support.google.com/accounts/answer/185839)
-and use [an application password](https://support.google.com/mail/answer/185833).
-
-To set up a basic Postfix mail server with IMAP access on Ubuntu, follow the
-[Postfix setup documentation](reply_by_email_postfix_setup.md).
-
-### Security Concerns
-
-**WARNING:** Be careful when choosing the domain used for receiving incoming
-email.
-
-For the sake of example, suppose your top-level company domain is `hooli.com`.
-All employees in your company have an email address at that domain via Google
-Apps, and your company's private Slack instance requires a valid `@hooli.com`
-email address in order to sign up.
-
-If you also host a public-facing GitLab instance at `hooli.com` and set your
-incoming email domain to `hooli.com`, an attacker could abuse the "Create new
-issue by email" or
-"[Create new merge request by email](../user/project/merge_requests/index.md#create-new-merge-requests-by-email)"
-features by using a project's unique address as the email when signing up for
-Slack, which would send a confirmation email, which would create a new issue or
-merge request on the project owned by the attacker, allowing them to click the
-confirmation link and validate their account on your company's private Slack
-instance.
-
-We recommend receiving incoming email on a subdomain, such as
-`incoming.hooli.com`, and ensuring that you do not employ any services that
-authenticate solely based on access to an email domain such as `*.hooli.com.`
-Alternatively, use a dedicated domain for GitLab email communications such as
-`hooli-gitlab.com`.
-
-See GitLab issue [#30366](https://gitlab.com/gitlab-org/gitlab-ce/issues/30366)
-for a real-world example of this exploit.
-
-### Omnibus package installations
-
-1. Find the `incoming_email` section in `/etc/gitlab/gitlab.rb`, enable the
- feature and fill in the details for your specific IMAP server and email account:
-
- ```ruby
- # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- gitlab_rails['incoming_email_address'] = "incoming+%{key}@gitlab.example.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- gitlab_rails['incoming_email_email'] = "incoming"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "gitlab.example.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 143
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = false
- # Whether the IMAP server uses StartTLS
- gitlab_rails['incoming_email_start_tls'] = false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- gitlab_rails['incoming_email_mailbox_name'] = "inbox"
- # The IDLE command timeout.
- gitlab_rails['incoming_email_idle_timeout'] = 60
- ```
-
- ```ruby
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- gitlab_rails['incoming_email_address'] = "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- gitlab_rails['incoming_email_email'] = "gitlab-incoming@gmail.com"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "imap.gmail.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 993
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = true
- # Whether the IMAP server uses StartTLS
- gitlab_rails['incoming_email_start_tls'] = false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- gitlab_rails['incoming_email_mailbox_name'] = "inbox"
- # The IDLE command timeout.
- gitlab_rails['incoming_email_idle_timeout'] = 60
- ```
-
- ```ruby
- # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
- gitlab_rails['incoming_email_enabled'] = true
-
- # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
- gitlab_rails['incoming_email_address'] = "incoming@exchange.example.com"
-
- # Email account username
- # Typically this is the userPrincipalName (UPN)
- gitlab_rails['incoming_email_email'] = "incoming@ad-domain.example.com"
- # Email account password
- gitlab_rails['incoming_email_password'] = "[REDACTED]"
-
- # IMAP server host
- gitlab_rails['incoming_email_host'] = "exchange.example.com"
- # IMAP server port
- gitlab_rails['incoming_email_port'] = 993
- # Whether the IMAP server uses SSL
- gitlab_rails['incoming_email_ssl'] = true
- ```
-
-1. Reconfigure GitLab for the changes to take effect:
-
- ```sh
- sudo gitlab-ctl reconfigure
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- sudo gitlab-rake gitlab:incoming_email:check
- ```
-
-1. Reply by email should now be working.
-
-### Installations from source
-
-1. Go to the GitLab installation directory:
-
- ```sh
- cd /home/git/gitlab
- ```
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature
- and fill in the details for your specific IMAP server and email account:
-
- ```sh
- sudo editor config/gitlab.yml
- ```
-
- ```yaml
- # Configuration for Postfix mail server, assumes mailbox incoming@gitlab.example.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "incoming+%{key}@gitlab.example.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "incoming"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "gitlab.example.com"
- # IMAP server port
- port: 143
- # Whether the IMAP server uses SSL
- ssl: false
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- # The IDLE command timeout.
- idle_timeout: 60
- ```
-
- ```yaml
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "gitlab-incoming@gmail.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "imap.gmail.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- # The IDLE command timeout.
- idle_timeout: 60
- ```
-
- ```yaml
- # Configuration for Microsoft Exchange mail server w/ IMAP enabled, assumes mailbox incoming@exchange.example.com
- incoming_email:
- enabled: true
-
- # The email address replies are sent to - Exchange does not support sub-addressing so %{key} is not used here
- address: "incoming@exchange.example.com"
-
- # Email account username
- # Typically this is the userPrincipalName (UPN)
- user: "incoming@ad-domain.example.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "exchange.example.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- # The IDLE command timeout.
- idle_timeout: 60
- ```
-
-1. Enable `mail_room` in the init script at `/etc/default/gitlab`:
-
- ```sh
- sudo mkdir -p /etc/default
- echo 'mail_room_enabled=true' | sudo tee -a /etc/default/gitlab
- ```
-
-1. Restart GitLab:
-
- ```sh
- sudo service gitlab restart
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- sudo -u git -H bundle exec rake gitlab:incoming_email:check RAILS_ENV=production
- ```
-
-1. Reply by email should now be working.
-
-### Development
-
-1. Go to the GitLab installation directory.
-
-1. Find the `incoming_email` section in `config/gitlab.yml`, enable the feature and fill in the details for your specific IMAP server and email account:
-
- ```yaml
- # Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
- incoming_email:
- enabled: true
-
- # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
- # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
- address: "gitlab-incoming+%{key}@gmail.com"
-
- # Email account username
- # With third party providers, this is usually the full email address.
- # With self-hosted email servers, this is usually the user part of the email address.
- user: "gitlab-incoming@gmail.com"
- # Email account password
- password: "[REDACTED]"
-
- # IMAP server host
- host: "imap.gmail.com"
- # IMAP server port
- port: 993
- # Whether the IMAP server uses SSL
- ssl: true
- # Whether the IMAP server uses StartTLS
- start_tls: false
-
- # The mailbox where incoming mail will end up. Usually "inbox".
- mailbox: "inbox"
- # The IDLE command timeout.
- idle_timeout: 60
- ```
-
- As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
-
-1. Uncomment the `mail_room` line in your `Procfile`:
-
- ```yaml
- mail_room: bundle exec mail_room -q -c config/mail_room.yml
- ```
-
-1. Restart GitLab:
-
- ```sh
- bundle exec foreman start
- ```
-
-1. Verify that everything is configured correctly:
-
- ```sh
- bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
- ```
-
-1. Reply by email should now be working.
diff --git a/doc/administration/reply_by_email_postfix_setup.md b/doc/administration/reply_by_email_postfix_setup.md
index a1bb3851951..3e8b78e56d5 100644
--- a/doc/administration/reply_by_email_postfix_setup.md
+++ b/doc/administration/reply_by_email_postfix_setup.md
@@ -1,7 +1,7 @@
-# Set up Postfix for Reply by email
+# Set up Postfix for incoming email
This document will take you through the steps of setting up a basic Postfix mail
-server with IMAP authentication on Ubuntu, to be used with [Reply by email].
+server with IMAP authentication on Ubuntu, to be used with [incoming email].
The instructions make the assumption that you will be using the email address `incoming@gitlab.example.com`, that is, username `incoming` on host `gitlab.example.com`. Don't forget to change it to your actual host when executing the example code snippets.
@@ -177,12 +177,12 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
```sh
sudo apt-get install courier-imap
```
-
+
And start `imapd`:
```sh
imapd start
```
-
+
1. The courier-authdaemon isn't started after installation. Without it, imap authentication will fail:
```sh
sudo service courier-authdaemon start
@@ -329,10 +329,10 @@ Courier, which we will install later to add IMAP authentication, requires mailbo
## Done!
-If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [Reply by email](./reply_by_email.md) guide to configure GitLab.
+If all the tests were successful, Postfix is all set up and ready to receive email! Continue with the [incoming email] guide to configure GitLab.
---
_This document was adapted from https://help.ubuntu.com/community/PostfixBasicSetupHowto, by contributors to the Ubuntu documentation wiki._
-[reply by email]: reply_by_email.md
+[incoming email]: incoming_email.md
diff --git a/doc/api/README.md b/doc/api/README.md
index b193ef4ab7f..53f1a70c1aa 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -24,6 +24,7 @@ following locations:
- [GitLab CI Config templates](templates/gitlab_ci_ymls.md)
- [Groups](groups.md)
- [Group Access Requests](access_requests.md)
+- [Group Badges](group_badges.md)
- [Group Members](members.md)
- [Issues](issues.md)
- [Issue Boards](boards.md)
@@ -43,6 +44,7 @@ following locations:
- [Pipeline Schedules](pipeline_schedules.md)
- [Projects](projects.md) including setting Webhooks
- [Project Access Requests](access_requests.md)
+- [Project Badges](project_badges.md)
- [Project import/export](project_import_export.md)
- [Project Members](members.md)
- [Project Snippets](project_snippets.md)
diff --git a/doc/api/branches.md b/doc/api/branches.md
index 80744258acb..01bb30c3859 100644
--- a/doc/api/branches.md
+++ b/doc/api/branches.md
@@ -13,6 +13,7 @@ GET /projects/:id/repository/branches
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `search` | string | no | Return list of branches matching the search criteria. |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/5/repository/branches
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 2c745d00887..55c673fd06a 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -14,6 +14,9 @@ GET /projects/:id/repository/commits
| `ref_name` | string | no | The name of a repository branch or tag or if not given the default branch |
| `since` | string | no | Only commits after or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
| `until` | string | no | Only commits before or on this date will be returned in ISO 8601 format YYYY-MM-DDTHH:MM:SSZ |
+| `path` | string | no | The file path |
+| `all` | boolean | no | Retrieve every commit from the repository |
+
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/5/repository/commits"
diff --git a/doc/api/group_badges.md b/doc/api/group_badges.md
new file mode 100644
index 00000000000..3e0683f378d
--- /dev/null
+++ b/doc/api/group_badges.md
@@ -0,0 +1,191 @@
+# Group badges API
+
+## Placeholder tokens
+
+Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are:
+
+- **%{project_path}**: will be replaced by the project path.
+- **%{project_id}**: will be replaced by the project id.
+- **%{default_branch}**: will be replaced by the project default branch.
+- **%{commit_sha}**: will be replaced by the last project's commit sha.
+
+Because these enpoints aren't inside a project's context, the information used to replace the placeholders will be
+from the first group's project by creation date. If the group hasn't got any project the original URL with the placeholders will be returned.
+
+## List all badges of a group
+
+Gets a list of a group's badges.
+
+```
+GET /groups/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+ },
+ {
+ "id": 2,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+ },
+]
+```
+
+## Get a badge of a group
+
+Gets a badge of a group.
+
+```
+GET /groups/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+}
+```
+
+## Add a badge to a group
+
+Adds a badge to a group.
+
+```
+POST /groups/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link |
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "link_url=https://gitlab.com/gitlab-org/gitlab-ce/commits/master&image_url=https://shields.io/my/badge1&position=0" https://gitlab.example.com/api/v4/groups/:id/badges
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge1",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge1",
+ "kind": "group"
+}
+```
+
+## Edit a badge of a group
+
+Updates a badge of a group.
+
+```
+PUT /groups/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+| `link_url` | string | no | URL of the badge link |
+| `image_url` | string | no | URL of the badge image |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+}
+```
+
+## Remove a badge from a group
+
+Removes a badge from a group.
+
+```
+DELETE /groups/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/:badge_id
+```
+
+## Preview a badge from a group
+
+Returns how the `link_url` and `image_url` final URLs would be after resolving the placeholder interpolation.
+
+```
+GET /groups/:id/badges/render
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link|
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/:id/badges/render?link_url=http%3A%2F%2Fexample.com%2Fci_status.svg%3Fproject%3D%25%7Bproject_path%7D%26ref%3D%25%7Bdefault_branch%7D&image_url=https%3A%2F%2Fshields.io%2Fmy%2Fbadge
+```
+
+Example response:
+
+```json
+{
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+}
+```
diff --git a/doc/api/groups.md b/doc/api/groups.md
index f50558b58a6..1aed8aac64e 100644
--- a/doc/api/groups.md
+++ b/doc/api/groups.md
@@ -525,3 +525,7 @@ And to switch pages add:
```
[ce-15142]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15142
+
+## Group badges
+
+Read more in the [Group Badges](group_badges.md) documentation.
diff --git a/doc/api/issues.md b/doc/api/issues.md
index da89db17cd9..a4a51101297 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -46,6 +46,10 @@ GET /issues?my_reaction_emoji=star
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search issues against their `title` and `description` |
+| `created_after` | datetime | no | Return issues created on or after the given time |
+| `created_before` | datetime | no | Return issues created on or before the given time |
+| `updated_after` | datetime | no | Return issues updated on or after the given time |
+| `updated_before` | datetime | no | Return issues updated on or before the given time |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/issues
@@ -152,6 +156,10 @@ GET /groups/:id/issues?my_reaction_emoji=star
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search group issues against their `title` and `description` |
+| `created_after` | datetime | no | Return issues created on or after the given time |
+| `created_before` | datetime | no | Return issues created on or before the given time |
+| `updated_after` | datetime | no | Return issues updated on or after the given time |
+| `updated_before` | datetime | no | Return issues updated on or before the given time |
```bash
@@ -259,8 +267,10 @@ GET /projects/:id/issues?my_reaction_emoji=star
| `order_by` | string | no | Return issues ordered by `created_at` or `updated_at` fields. Default is `created_at` |
| `sort` | string | no | Return issues sorted in `asc` or `desc` order. Default is `desc` |
| `search` | string | no | Search project issues against their `title` and `description` |
-| `created_after` | datetime | no | Return issues created after the given time (inclusive) |
-| `created_before` | datetime | no | Return issues created before the given time (inclusive) |
+| `created_after` | datetime | no | Return issues created on or after the given time |
+| `created_before` | datetime | no | Return issues created on or before the given time |
+| `updated_after` | datetime | no | Return issues updated on or after the given time |
+| `updated_before` | datetime | no | Return issues updated on or before the given time |
```bash
curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/4/issues
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index 2957a0a5f48..25b0807eb18 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -41,12 +41,16 @@ Parameters:
| `milestone` | string | no | Return merge requests for a specific milestone |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
-| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) |
-| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) |
+| `created_after` | datetime | no | Return merge requests created on or after the given time |
+| `created_before` | datetime | no | Return merge requests created on or before the given time |
+| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
+| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all`. Defaults to `created-by-me` |
| `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)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
@@ -156,12 +160,16 @@ Parameters:
| `milestone` | string | no | Return merge requests for a specific milestone |
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels |
-| `created_after` | datetime | no | Return merge requests created after the given time (inclusive) |
-| `created_before` | datetime | no | Return merge requests created before the given time (inclusive) |
+| `created_after` | datetime | no | Return merge requests created on or after the given time |
+| `created_before` | datetime | no | Return merge requests created on or before the given time |
+| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
+| `updated_before` | datetime | no | Return merge requests updated on or before the given time |
| `scope` | string | no | Return merge requests for the given scope: `created-by-me`, `assigned-to-me` or `all` _([Introduced][ce-13060] in GitLab 9.5)_ |
| `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)_ |
+| `source_branch` | string | no | Return merge requests with the given source branch |
+| `target_branch` | string | no | Return merge requests with the given target branch |
| `search` | string | no | Search merge requests against their `title` and `description` |
```json
@@ -257,20 +265,20 @@ Parameters:
"upvotes": 0,
"downvotes": 0,
"author": {
- "id": 1,
- "username": "admin",
- "email": "admin@example.com",
- "name": "Administrator",
- "state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
},
"assignee": {
- "id": 1,
- "username": "admin",
- "email": "admin@example.com",
- "name": "Administrator",
- "state": "active",
- "created_at": "2012-04-29T08:46:00Z"
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
},
"source_project_id": 2,
"target_project_id": 3,
@@ -304,6 +312,26 @@ Parameters:
"total_time_spent": 0,
"human_time_estimate": null,
"human_total_time_spent": null
+ },
+ "closed_at": "2018-01-19T14:36:11.086Z",
+ "latest_build_started_at": null,
+ "latest_build_finished_at": null,
+ "first_deployed_to_production_at": null,
+ "pipeline": {
+ "id": 8,
+ "ref": "master",
+ "sha": "2dc6aa325a317eda67812f05600bdf0fcdc70ab0",
+ "status": "created"
+ },
+ "merged_by": null,
+ "merged_at": null,
+ "closed_by": {
+ "state" : "active",
+ "web_url" : "https://gitlab.example.com/root",
+ "avatar_url" : null,
+ "username" : "root",
+ "id" : 1,
+ "name" : "Administrator"
}
}
```
@@ -470,6 +498,8 @@ Parameters:
## List MR pipelines
+> [Introduced][ce-15454] in GitLab 10.5.0.
+
Get a list of merge request pipelines.
```
@@ -1425,3 +1455,4 @@ Example response:
[ce-13060]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/13060
[ce-14016]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14016
+[ce-15454]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15454
diff --git a/doc/api/project_badges.md b/doc/api/project_badges.md
new file mode 100644
index 00000000000..3f6e348b5b4
--- /dev/null
+++ b/doc/api/project_badges.md
@@ -0,0 +1,188 @@
+# Project badges API
+
+## Placeholder tokens
+
+Badges support placeholders that will be replaced in real time in both the link and image URL. The allowed placeholders are:
+
+- **%{project_path}**: will be replaced by the project path.
+- **%{project_id}**: will be replaced by the project id.
+- **%{default_branch}**: will be replaced by the project default branch.
+- **%{commit_sha}**: will be replaced by the last project's commit sha.
+
+## List all badges of a project
+
+Gets a list of a project's badges and its group badges.
+
+```
+GET /projects/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges
+```
+
+Example response:
+
+```json
+[
+ {
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "project"
+ },
+ {
+ "id": 2,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "group"
+ },
+]
+```
+
+## Get a badge of a project
+
+Gets a badge of a project.
+
+```
+GET /projects/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "project"
+}
+```
+
+## Add a badge to a project
+
+Adds a badge to a project.
+
+```
+POST /projects/:id/badges
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project ](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link |
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --data "link_url=https://gitlab.com/gitlab-org/gitlab-ce/commits/master&image_url=https://shields.io/my/badge1&position=0" https://gitlab.example.com/api/v4/projects/:id/badges
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge1",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge1",
+ "kind": "project"
+}
+```
+
+## Edit a badge of a project
+
+Updates a badge of a project.
+
+```
+PUT /projects/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+| `link_url` | string | no | URL of the badge link |
+| `image_url` | string | no | URL of the badge image |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/:badge_id
+```
+
+Example response:
+
+```json
+{
+ "id": 1,
+ "link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "https://gitlab.com/gitlab-org/gitlab-ce/commits/master",
+ "rendered_image_url": "https://shields.io/my/badge",
+ "kind": "project"
+}
+```
+
+## Remove a badge from a project
+
+Removes a badge from a project. Only project's badges will be removed by using this endpoint.
+
+```
+DELETE /projects/:id/badges/:badge_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `badge_id` | integer | yes | The badge ID |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/:badge_id
+```
+
+## Preview a badge from a project
+
+Returns how the `link_url` and `image_url` final URLs would be after resolving the placeholder interpolation.
+
+```
+GET /projects/:id/badges/render
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `link_url` | string | yes | URL of the badge link|
+| `image_url` | string | yes | URL of the badge image |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/projects/:id/badges/render?link_url=http%3A%2F%2Fexample.com%2Fci_status.svg%3Fproject%3D%25%7Bproject_path%7D%26ref%3D%25%7Bdefault_branch%7D&image_url=https%3A%2F%2Fshields.io%2Fmy%2Fbadge
+```
+
+Example response:
+
+```json
+{
+ "link_url": "http://example.com/ci_status.svg?project=%{project_path}&ref=%{default_branch}",
+ "image_url": "https://shields.io/my/badge",
+ "rendered_link_url": "http://example.com/ci_status.svg?project=example-org/example-project&ref=master",
+ "rendered_image_url": "https://shields.io/my/badge",
+}
+```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 9e649efea9c..271ee91dc72 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -1194,6 +1194,7 @@ GET /projects/:id/hooks/:hook_id
"project_id": 3,
"push_events": true,
"issues_events": true,
+ "confidential_issues_events": true,
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
@@ -1219,12 +1220,13 @@ POST /projects/:id/hooks
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
+| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
| `job_events` | boolean | no | Trigger hook on job events |
| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
-| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `wiki_page_events` | boolean | no | Trigger hook on wiki events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
| `token` | string | no | Secret token to validate received payloads; this will not be returned in the response |
@@ -1243,6 +1245,7 @@ PUT /projects/:id/hooks/:hook_id
| `url` | string | yes | The hook URL |
| `push_events` | boolean | no | Trigger hook on push events |
| `issues_events` | boolean | no | Trigger hook on issues events |
+| `confidential_issues_events` | boolean | no | Trigger hook on confidential issues events |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
| `tag_push_events` | boolean | no | Trigger hook on tag push events |
| `note_events` | boolean | no | Trigger hook on note events |
@@ -1337,3 +1340,7 @@ Read more in the [Project import/export](project_import_export.md) documentation
## Project members
Read more in the [Project members](members.md) documentation.
+
+## Project badges
+
+Read more in the [Project Badges](project_badges.md) documentation.
diff --git a/doc/api/services.md b/doc/api/services.md
index 2928ab6cc75..92f12acbc73 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -619,6 +619,7 @@ Example response:
"active": true,
"push_events": true,
"issues_events": true,
+ "confidential_issues_events": true,
"merge_requests_events": true,
"tag_push_events": true,
"note_events": true,
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index ac4a9b0ed27..856d7f264e4 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -121,8 +121,9 @@ The basic requirements is that there are two numbers separated with one of
the following (you can even use them interchangeably):
- a space
-- a backslash (`/`)
+- a forward slash (`/`)
- a colon (`:`)
+- a dot (`.`)
>**Note:**
More specifically, [it uses][regexp] this regular expression: `\d+[\s:\/\\]+\d+\s*`.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index f30a85b114e..23ce6a5f210 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -56,6 +56,9 @@ future GitLab releases.**
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **CI_RUNNER_VERSION** | all | 10.6 | GitLab Runner version that is executing the current job |
+| **CI_RUNNER_REVISION** | all | 10.6 | GitLab Runner revision that is executing the current job |
+| **CI_RUNNER_EXECUTABLE_ARCH** | all | 10.6 | The OS/architecture of the GitLab Runner executable (note that this is not necessarily the same as the environment of the executor) |
| **CI_PIPELINE_ID** | 8.10 | 0.5 | The unique id of the current pipeline that GitLab CI uses internally |
| **CI_PIPELINE_TRIGGERED** | all | all | The flag to indicate that job was [triggered] |
| **CI_PIPELINE_SOURCE** | 10.0 | all | The source for this pipeline, one of: push, web, trigger, schedule, api, external. Pipelines created before 9.5 will have unknown as source |
diff --git a/doc/development/database_debugging.md b/doc/development/database_debugging.md
index 50eb8005b44..32f392f1303 100644
--- a/doc/development/database_debugging.md
+++ b/doc/development/database_debugging.md
@@ -53,3 +53,38 @@ bundle exec rails db RAILS_ENV=development
- `CREATE TABLE board_labels();`: Create a table called `board_labels`
- `SELECT * FROM schema_migrations WHERE version = '20170926203418';`: Check if a migration was run
- `DELETE FROM schema_migrations WHERE version = '20170926203418';`: Manually remove a migration
+
+
+## FAQ
+
+### `ActiveRecord::PendingMigrationError` with Spring
+
+When running specs with the [Spring preloader](./rake_tasks.md#speed-up-tests-rake-tasks-and-migrations),
+the test database can get into a corrupted state. Trying to run the migration or
+dropping/resetting the test database has no effect.
+
+```sh
+$ bundle exec spring rspec some_spec.rb
+...
+Failure/Error: ActiveRecord::Migration.maintain_test_schema!
+
+ActiveRecord::PendingMigrationError:
+
+
+ Migrations are pending. To resolve this issue, run:
+
+ bin/rake db:migrate RAILS_ENV=test
+# ~/.rvm/gems/ruby-2.3.3/gems/activerecord-4.2.10/lib/active_record/migration.rb:392:in `check_pending!'
+...
+0 examples, 0 failures, 1 error occurred outside of examples
+```
+
+To resolve, you can kill the spring server and app that lives between spec runs.
+
+```sh
+$ ps aux | grep spring
+eric 87304 1.3 2.9 3080836 482596 ?? Ss 10:12AM 4:08.36 spring app | gitlab | started 6 hours ago | test mode
+eric 37709 0.0 0.0 2518640 7524 s006 S Wed11AM 0:00.79 spring server | gitlab | started 29 hours ago
+$ kill 87304
+$ kill 37709
+```
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index f6a14de96b2..1eb90c30ebd 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -33,6 +33,40 @@ rest of the code should be as close to the CE files as possible.
[single code base]: https://gitlab.com/gitlab-org/gitlab-ee/issues/2952#note_41016454
+### Detection of EE-only files
+
+For each commit (except on `master`), the `ee-files-location-check` CI job tries
+to detect if there are any new files that are EE-only. If any file is detected,
+the job fails with an explanation of why and what to do to make it pass.
+
+Basically, the fix is simple: `git mv <file> ee/<file>`.
+
+#### How to name your branches?
+
+For any EE branch, the job will try to detect its CE counterpart by removing any
+`ee-` prefix or `-ee` suffix from the EE branch name, and matching the last
+branch that contains it.
+
+For instance, from the EE branch `new-shiny-feature-ee` (or
+`ee-new-shiny-feature`), the job would find the corresponding CE branches:
+
+- `new-shiny-feature`
+- `ce-new-shiny-feature`
+- `new-shiny-feature-ce`
+- `my-super-new-shiny-feature-in-ce`
+
+#### Whitelist some EE-only files that cannot be moved to `ee/`
+
+The `ee-files-location-check` CI job provides a whitelist of files or folders
+that cannot or should not be moved to `ee/`. Feel free to open an issue to
+discuss adding a new file/folder to this whitelist.
+
+For instance, it was decided that moving EE-only files from `qa/` to `ee/qa/`
+would make it difficult to build the `gitLab-{ce,ee}-qa` Docker images and it
+was [not worth the complexity].
+
+[not worth the complexity]: https://gitlab.com/gitlab-org/gitlab-ee/issues/4997#note_59764702
+
### EE-only features
If the feature being developed is not present in any form in CE, we don't
@@ -52,6 +86,11 @@ is applied not only to models. Here's a list of other examples:
- `ee/app/validators/foo_attr_validator.rb`
- `ee/app/workers/foo_worker.rb`
+This works because for every path that are present in CE's eager-load/auto-load
+paths, we add the same `ee/`-prepended path in [`config/application.rb`].
+
+[`config/application.rb`]: https://gitlab.com/gitlab-org/gitlab-ee/blob/d278b76d6600a0e27d8019a0be27971ba23ab640/config/application.rb#L41-51
+
### EE features based on CE features
For features that build on existing CE features, write a module in the
diff --git a/doc/development/emails.md b/doc/development/emails.md
index 18f47f44cb5..677029b1295 100644
--- a/doc/development/emails.md
+++ b/doc/development/emails.md
@@ -18,6 +18,68 @@ See the [Rails guides] for more info.
[previews]: https://gitlab.com/gitlab-org/gitlab-ce/tree/master/spec/mailers/previews
[Rails guides]: http://guides.rubyonrails.org/action_mailer_basics.html#previewing-emails
+## Incoming email
+
+1. Go to the GitLab installation directory.
+
+1. Find the `incoming_email` section in `config/gitlab.yml`, enable the
+ feature and fill in the details for your specific IMAP server and email
+ account:
+
+ Configuration for Gmail / Google Apps, assumes mailbox gitlab-incoming@gmail.com
+
+ ```yaml
+ incoming_email:
+ enabled: true
+
+ # The email address including the `%{key}` placeholder that will be replaced to reference the item being replied to.
+ # The placeholder can be omitted but if present, it must appear in the "user" part of the address (before the `@`).
+ address: "gitlab-incoming+%{key}@gmail.com"
+
+ # Email account username
+ # With third party providers, this is usually the full email address.
+ # With self-hosted email servers, this is usually the user part of the email address.
+ user: "gitlab-incoming@gmail.com"
+ # Email account password
+ password: "[REDACTED]"
+
+ # IMAP server host
+ host: "imap.gmail.com"
+ # IMAP server port
+ port: 993
+ # Whether the IMAP server uses SSL
+ ssl: true
+ # Whether the IMAP server uses StartTLS
+ start_tls: false
+
+ # The mailbox where incoming mail will end up. Usually "inbox".
+ mailbox: "inbox"
+ # The IDLE command timeout.
+ idle_timeout: 60
+ ```
+
+ As mentioned, the part after `+` is ignored, and this will end up in the mailbox for `gitlab-incoming@gmail.com`.
+
+1. Uncomment the `mail_room` line in your `Procfile`:
+
+ ```yaml
+ mail_room: bundle exec mail_room -q -c config/mail_room.yml
+ ```
+
+1. Restart GitLab:
+
+ ```sh
+ bundle exec foreman start
+ ```
+
+1. Verify that everything is configured correctly:
+
+ ```sh
+ bundle exec rake gitlab:incoming_email:check RAILS_ENV=development
+ ```
+
+1. Reply by email should now be working.
+
---
[Return to Development documentation](README.md)
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index 14ac1133cc0..98e43931a02 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -36,6 +36,15 @@ If you are asynchronously adding content which contains lazy images then you nee
`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed.
But in general it should be handled automatically through a `MutationObserver` in the lazy loading function.
+### Animations
+
+Only animate `opacity` & `transform` properties. Other properties (such as `top`, `left`, `margin`, and `padding`) all cause
+Layout to be recalculated, which is much more expensive. For details on this, see "Styles that Affect Layout" in
+[High Performance Animations][high-perf-animations].
+
+If you _do_ need to change layout (e.g. a sidebar that pushes main content over), prefer [FLIP][flip] to change expensive
+properties once, and handle the actual animation with transforms.
+
## Reducing Asset Footprint
### Page-specific JavaScript
@@ -87,6 +96,7 @@ General tips:
- Compress and minify assets wherever possible (For CSS/JS, Sprockets and webpack do this for us).
- If some functionality can reasonably be achieved without adding extra libraries, avoid them.
- Use page-specific JavaScript as described above to dynamically load libraries that are only needed on certain pages.
+- [High Performance Animations][high-perf-animations]
-------
@@ -105,3 +115,5 @@ General tips:
[d3]: https://d3js.org/
[chartjs]: http://www.chartjs.org/
[page-specific-js-example]: https://gitlab.com/gitlab-org/gitlab-ce/blob/13bb9ed77f405c5f6ee4fdbc964ecf635c9a223f/app/views/projects/graphs/_head.html.haml#L6-8
+[high-perf-animations]: https://www.html5rocks.com/en/tutorials/speed/high-performance-animations/
+[flip]: https://aerotwist.com/blog/flip-your-animations/
diff --git a/doc/development/fe_guide/vue.md b/doc/development/fe_guide/vue.md
index 6c93c29124d..093a3ca4407 100644
--- a/doc/development/fe_guide/vue.md
+++ b/doc/development/fe_guide/vue.md
@@ -507,13 +507,15 @@ This is the entry point for our store. You can use the following as a guide:
import Vue from 'vue';
import Vuex from 'vuex';
import * as actions from './actions';
-import * as mutations from './mutations';
+import * as getters from './getters';
+import mutations from './mutations';
Vue.use(Vuex);
export default new Vuex.Store({
actions,
getters,
+ mutations,
state: {
users: [],
},
@@ -525,7 +527,7 @@ _Note:_ If the state of the application is too complex, an individual file for t
An action commits a mutatation. In this file, we will write the actions that will call the respective mutation:
```javascript
- import * as types from './mutation-types'
+ import * as types from './mutation_types';
export const addUser = ({ commit }, user) => {
commit(types.ADD_USER, user);
@@ -575,7 +577,8 @@ import { mapGetters } from 'vuex';
The only way to actually change state in a Vuex store is by committing a mutation.
```javascript
- import * as types from './mutation-types'
+ import * as types from './mutation_types';
+
export default {
[types.ADD_USER](state, user) {
state.users.push(user);
@@ -684,4 +687,3 @@ describe('component', () => {
[vuex-testing]: https://vuex.vuejs.org/en/testing.html
[axios]: https://github.com/axios/axios
[axios-interceptors]: https://github.com/axios/axios#interceptors
-
diff --git a/doc/development/i18n/externalization.md b/doc/development/i18n/externalization.md
index c0a325a83e9..c0ce49eb40b 100644
--- a/doc/development/i18n/externalization.md
+++ b/doc/development/i18n/externalization.md
@@ -107,104 +107,28 @@ You can mark that content for translation with:
### JavaScript files
-In JavaScript we added the `__()` (double underscore parenthesis) function
-for translations.
+In JavaScript we added the `__()` (double underscore parenthesis) function that
+you can import from the `~/locale` file. For instance:
-In order to test JavaScript translations you have to change the GitLab localization to other language than English and you have to generate JSON files using `bundle exec rake gettext:po_to_json` or `bundle exec rake gettext:compile`.
-
-## Updating the PO files with the new content
-
-Now that the new content is marked for translation, we need to update the PO
-files with the following command:
-
-```sh
-bundle exec rake gettext:find
-```
-
-This command will update the `locale/gitlab.pot` file with the newly externalized
-strings and remove any strings that aren't used anymore. You should check this
-file in. Once the changes are on master, they will be picked up by
-[Crowdin](http://translate.gitlab.com) and be presented for translation.
-
-If there are merge conflicts in the `gitlab.pot` file, you can delete the file
-and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
-
-The command also updates the translation files for each language: `locale/*/gitlab.po`
-These changes can be discarded, the languange files will be updated by Crowdin
-automatically.
-
-Discard all of them at once like this:
-
-```sh
-git checkout locale/*/gitlab.po
-```
-
-### Validating PO files
-
-To make sure we keep our translation files up to date, there's a linter that is
-running on CI as part of the `static-analysis` job.
-
-To lint the adjustments in PO files locally you can run `rake gettext:lint`.
-
-The linter will take the following into account:
-
-- Valid PO-file syntax
-- Variable usage
- - Only one unnamed (`%d`) variable, since the order of variables might change
- in different languages
- - All variables used in the message-id are used in the translation
- - There should be no variables used in a translation that aren't in the
- message-id
-- Errors during translation.
-
-The errors are grouped per file, and per message ID:
-
-```
-Errors in `locale/zh_HK/gitlab.po`:
- PO-syntax errors
- SimplePoParser::ParserErrorSyntax error in lines
- Syntax error in msgctxt
- Syntax error in msgid
- Syntax error in msgstr
- Syntax error in message_line
- There should be only whitespace until the end of line after the double quote character of a message text.
- Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
- SimplePoParser filtered backtrace: SimplePoParser::ParserError
-Errors in `locale/zh_TW/gitlab.po`:
- 1 pipeline
- <%d 條流水線> is using unknown variables: [%d]
- Failure translating to zh_TW with []: too few arguments
+```js
+import { __ } from '~/locale';
+const label = __('Subscribe');
```
-In this output the `locale/zh_HK/gitlab.po` has syntax errors.
-The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
-aren't in the message with id `1 pipeline`.
-
-## Working with special content
-
-
-### Just marking content for parsing
-
-- In Ruby/HAML:
-
- ```ruby
- _('Subscribe')
- ```
-
-- In JavaScript:
-
- ```js
- import { __ } from '../../../locale';
- const label = __('Subscribe');
- ```
+In order to test JavaScript translations you have to change the GitLab
+localization to other language than English and you have to generate JSON files
+using `bin/rake gettext:po_to_json` or `bin/rake gettext:compile`.
+### Dynamic translations
Sometimes there are some dynamic translations that can't be found by the
-parser when running `bundle exec rake gettext:find`. For these scenarios you can
-use the [`_N` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
+parser when running `bin/rake gettext:find`. For these scenarios you can
+use the [`N_` method](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#unfound-translations-with-rake-gettextfind).
There is also and alternative method to [translate messages from validation errors](https://github.com/grosser/gettext_i18n_rails/blob/c09e38d481e0899ca7d3fc01786834fa8e7aab97/Readme.md#option-a).
+## Working with special content
+
### Interpolation
- In Ruby/HAML:
@@ -216,7 +140,7 @@ There is also and alternative method to [translate messages from validation erro
- In JavaScript:
```js
- import { __, sprintf } from '../../../locale';
+ import { __, sprintf } from '~/locale';
sprintf(__('Hello %{username}'), { username: 'Joe' }) => 'Hello Joe'
```
@@ -228,24 +152,30 @@ For example use `%{created_at}` in Ruby but `%{createdAt}` in JavaScript.
- In Ruby/HAML:
```ruby
- n_('Apple', 'Apples', 3) => 'Apples'
+ n_('Apple', 'Apples', 3)
+ # => 'Apples'
```
Using interpolation:
```ruby
n_("There is a mouse.", "There are %d mice.", size) % size
+ # => When size == 1: 'There is a mouse.'
+ # => When size == 2: 'There are 2 mice.'
```
- In JavaScript:
```js
- n__('Apple', 'Apples', 3) => 'Apples'
+ n__('Apple', 'Apples', 3)
+ // => 'Apples'
```
Using interpolation:
```js
- n__('Last day', 'Last %d days', 30) => 'Last 30 days'
+ n__('Last day', 'Last %d days', x)
+ // => When x == 1: 'Last day'
+ // => When x == 2: 'Last 2 days'
```
### Namespaces
@@ -267,12 +197,15 @@ Sometimes you need to add some context to the text that you want to translate
s__('OpenedNDaysAgo|Opened')
```
+Note: The namespace should be removed from the translation. See the [translation
+guidelines for more details](./translation.md#namespaced-strings).
+
### Dates / times
- In JavaScript:
```js
-import { createDateTimeFormat } from '.../locale';
+import { createDateTimeFormat } from '~/locale';
const dateFormat = createDateTimeFormat({ year: 'numeric', month: 'long', day: 'numeric' });
console.log(dateFormat.format(new Date('2063-04-05'))) // April 5, 2063
@@ -282,6 +215,100 @@ This makes use of [`Intl.DateTimeFormat`].
[`Intl.DateTimeFormat`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
+## Best practices
+
+### Splitting sentences
+
+Please never split a sentence as that would assume the sentence grammar and
+structure is the same in all languages.
+
+For instance, the following
+
+```js
+{{ s__("mrWidget|Set by") }}
+{{ author.name }}
+{{ s__("mrWidget|to be merged automatically when the pipeline succeeds") }}
+```
+
+should be externalized as follows:
+
+```js
+{{ sprintf(s__("mrWidget|Set by %{author} to be merged automatically when the pipeline succeeds"), { author: author.name }) }}
+```
+
+When in doubt, try to follow the best practices described in this [Mozilla
+Developer documentation][mdn].
+
+[mdn]: https://developer.mozilla.org/en-US/docs/Mozilla/Localization/Localization_content_best_practices#Splitting
+
+## Updating the PO files with the new content
+
+Now that the new content is marked for translation, we need to update the PO
+files with the following command:
+
+```sh
+bin/rake gettext:find
+```
+
+This command will update the `locale/gitlab.pot` file with the newly externalized
+strings and remove any strings that aren't used anymore. You should check this
+file in. Once the changes are on master, they will be picked up by
+[Crowdin](http://translate.gitlab.com) and be presented for translation.
+
+If there are merge conflicts in the `gitlab.pot` file, you can delete the file
+and regenerate it using the same command. Confirm that you are not deleting any strings accidentally by looking over the diff.
+
+The command also updates the translation files for each language: `locale/*/gitlab.po`
+These changes can be discarded, the languange files will be updated by Crowdin
+automatically.
+
+Discard all of them at once like this:
+
+```sh
+git checkout locale/*/gitlab.po
+```
+
+### Validating PO files
+
+To make sure we keep our translation files up to date, there's a linter that is
+running on CI as part of the `static-analysis` job.
+
+To lint the adjustments in PO files locally you can run `rake gettext:lint`.
+
+The linter will take the following into account:
+
+- Valid PO-file syntax
+- Variable usage
+ - Only one unnamed (`%d`) variable, since the order of variables might change
+ in different languages
+ - All variables used in the message-id are used in the translation
+ - There should be no variables used in a translation that aren't in the
+ message-id
+- Errors during translation.
+
+The errors are grouped per file, and per message ID:
+
+```
+Errors in `locale/zh_HK/gitlab.po`:
+ PO-syntax errors
+ SimplePoParser::ParserErrorSyntax error in lines
+ Syntax error in msgctxt
+ Syntax error in msgid
+ Syntax error in msgstr
+ Syntax error in message_line
+ There should be only whitespace until the end of line after the double quote character of a message text.
+ Parseing result before error: '{:msgid=>["", "You are going to remove %{project_name_with_namespace}.\\n", "Removed project CANNOT be restored!\\n", "Are you ABSOLUTELY sure?"]}'
+ SimplePoParser filtered backtrace: SimplePoParser::ParserError
+Errors in `locale/zh_TW/gitlab.po`:
+ 1 pipeline
+ <%d 條流水線> is using unknown variables: [%d]
+ Failure translating to zh_TW with []: too few arguments
+```
+
+In this output the `locale/zh_HK/gitlab.po` has syntax errors.
+The `locale/zh_TW/gitlab.po` has variables that are used in the translation that
+aren't in the message with id `1 pipeline`.
+
## Adding a new language
Let's suppose you want to add translations for a new language, let's say French.
@@ -300,14 +327,14 @@ Let's suppose you want to add translations for a new language, let's say French.
1. Next, you need to add the language:
```sh
- bundle exec rake gettext:add_language[fr]
+ bin/rake gettext:add_language[fr]
```
If you want to add a new language for a specific region, the command is similar,
you just need to separate the region with an underscore (`_`). For example:
```sh
- bundle exec rake gettext:add_language[en_GB]
+ bin/rake gettext:add_language[en_GB]
```
Please note that you need to specify the region part in capitals.
@@ -321,7 +348,7 @@ Let's suppose you want to add translations for a new language, let's say French.
containing the translations:
```sh
- bundle exec rake gettext:compile
+ bin/rake gettext:compile
```
1. In order to see the translated content we need to change our preferred language
diff --git a/doc/development/i18n/proofreader.md b/doc/development/i18n/proofreader.md
index ece9a9bc0fe..b732cc65b73 100644
--- a/doc/development/i18n/proofreader.md
+++ b/doc/development/i18n/proofreader.md
@@ -15,12 +15,15 @@ are very appreciative of the work done by translators and proofreaders!
- Dutch
- Esperanto
- French
+ - Rémy Coutable - [GitLab](https://gitlab.com/rymai), [Crowdin](https://crowdin.com/profile/rymai)
- German
- Italian
- Paolo Falomo - [GitLab](https://gitlab.com/paolofalomo), [Crowdin](https://crowdin.com/profile/paolo.falomo)
- Japanese
- Korean
- Huang Tao - [GitLab](https://gitlab.com/htve), [Crowdin](https://crowdin.com/profile/htve)
+- Polish
+ - Filip Mech - [GitLab](https://gitlab.com/mehenz), [Crowdin](https://crowdin.com/profile/mehenz)
- Portuguese, Brazilian
- Paulo George Gomes Bezerra - [GitLab](https://gitlab.com/paulobezerra), [Crowdin](https://crowdin.com/profile/paulogomes.rep)
- Russian
diff --git a/doc/development/i18n/translation.md b/doc/development/i18n/translation.md
index b34ec754742..99c0fe6db1d 100644
--- a/doc/development/i18n/translation.md
+++ b/doc/development/i18n/translation.md
@@ -37,33 +37,43 @@ Comments can be added to discuss a translation with the community.
Remember to **Save** each translation.
-## Translation Guidelines
+## General Translation Guidelines
Be sure to check the following guidelines before you translate any strings.
+### Namespaced strings
+
+When an externalized string is prepended with a namespace, e.g.
+`s_('OpenedNDaysAgo|Opened')`, the namespace should be removed from the final
+translation.
+For example in French `OpenedNDaysAgo|Opened` would be translated to
+`Ouvert•e`, not `OpenedNDaysAgo|Ouvert•e`.
+
### Technical terms
-Technical terms should be treated like proper nouns and not be translated.
-This helps maintain a logical connection and consistency between tools (e.g. `git` client) and
-GitLab.
+Some technical terms should be treated like proper nouns and not be translated.
-Technical terms that should always be in English are noted in the glossary when using
-[translate.gitlab.com](https://translate.gitlab.com).
+Technical terms that should always be in English are noted in the glossary when
+using [translate.gitlab.com](https://translate.gitlab.com).
+
+This helps maintain a logical connection and consistency between tools (e.g.
+`git` client) and GitLab.
### Formality
The level of formality used in software varies by language.
-For example, in French we translate `you` as the informal `tu`.
+For example, in French we translate `you` as the formal `vous`.
-You can refer to other translated strings and notes in the glossary to assist determining a
-suitable level of formality.
+You can refer to other translated strings and notes in the glossary to assist
+determining a suitable level of formality.
### Inclusive language
[Diversity] is one of GitLab's values.
-We ask you to avoid translations which exclude people based on their gender or ethnicity.
-In languages which distinguish between a male and female form,
-use both or choose a neutral formulation.
+We ask you to avoid translations which exclude people based on their gender or
+ethnicity.
+In languages which distinguish between a male and female form, use both or
+choose a neutral formulation.
For example in German, the word "user" can be translated into "Benutzer" (male) or "Benutzerin" (female).
Therefore "create a new user" would translate into "Benutzer(in) anlegen".
@@ -74,3 +84,14 @@ Therefore "create a new user" would translate into "Benutzer(in) anlegen".
To propose additions to the glossary please
[open an issue](https://gitlab.com/gitlab-org/gitlab-ce/issues).
+
+## French Translation Guidelines
+
+### Inclusive language in French
+
+In French, we should follow the guidelines from [ecriture-inclusive.fr]. For
+instance:
+
+- Utilisateur•rice•s
+
+[ecriture-inclusive.fr]: http://www.ecriture-inclusive.fr/
diff --git a/doc/development/rake_tasks.md b/doc/development/rake_tasks.md
index dc88ce1522c..fdfa1f10402 100644
--- a/doc/development/rake_tasks.md
+++ b/doc/development/rake_tasks.md
@@ -102,6 +102,12 @@ variable to `1`:
export ENABLE_SPRING=1
```
+Alternatively you can use the following on each spec run,
+
+```
+bundle exec spring rspec some_spec.rb
+```
+
## Compile Frontend Assets
You shouldn't ever need to compile frontend assets manually in development, but
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 4dfc03d0fe0..170d92faa09 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -93,9 +93,9 @@ Is the system packaged Git too old? Remove it and compile from source.
# Download and compile from source
cd /tmp
- curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.14.3.tar.gz
- echo '023ffff6d3ba8a1bea779dfecc0ed0bb4ad68ab8601d14435dd8c08416f78d7f git-2.14.3.tar.gz' | shasum -a256 -c - && tar -xzf git-2.14.3.tar.gz
- cd git-2.14.3/
+ curl --remote-name --progress https://www.kernel.org/pub/software/scm/git/git-2.16.2.tar.gz
+ echo '9acc4339b7a2ab484eea69d705923271682b7058015219cf5a7e6ed8dee5b5fb git-2.16.2.tar.gz' | shasum -a256 -c - && tar -xzf git-2.16.2.tar.gz
+ cd git-2.16.2/
./configure
make prefix=/usr/local all
diff --git a/doc/integration/saml.md b/doc/integration/saml.md
index 3ae98adc465..f8a7dd6b1dc 100644
--- a/doc/integration/saml.md
+++ b/doc/integration/saml.md
@@ -109,8 +109,7 @@ in your SAML IdP:
1. Change the value of `issuer` to a unique name, which will identify the application
to the IdP.
-1. [Reconfigure][] or [restart GitLab][] for the changes to take effect if you
- installed GitLab via Omnibus or from source respectively.
+1. For the changes to take effect, you must [reconfigure][] GitLab if you installed via Omnibus or [restart GitLab][] if you installed from source.
1. Register the GitLab SP in your SAML 2.0 IdP, using the application name specified
in `issuer`.
diff --git a/doc/ssh/README.md b/doc/ssh/README.md
index 33a2d7a88a7..aa14a39e4c9 100644
--- a/doc/ssh/README.md
+++ b/doc/ssh/README.md
@@ -35,8 +35,8 @@ to clipboard step.
If you don't see the string or would like to generate a SSH key pair with a
custom name continue onto the next step.
->
-**Note:** Public SSH key may also be named as follows:
+Note that Public SSH key may also be named as follows:
+
- `id_dsa.pub`
- `id_ecdsa.pub`
- `id_ed25519.pub`
@@ -73,7 +73,7 @@ custom name continue onto the next step.
key pair, but it is not required and you can skip creating a password by
pressing enter.
- >**Note:**
+ NOTE: **Note:**
If you want to change the password of your SSH key pair, you can use
`ssh-keygen -p <keyname>`.
@@ -162,11 +162,13 @@ That's why it needs to uniquely map to a single user.
## Deploy keys
+### Per-repository deploy keys
+
Deploy keys allow read-only or read-write (if enabled) access to one or
multiple projects with a single SSH key pair.
This is really useful for cloning repositories to your Continuous
-Integration (CI) server. By using deploy keys, you don't have to setup a
+Integration (CI) server. By using deploy keys, you don't have to set up a
dummy user account.
If you are a project master or owner, you can add a deploy key in the
@@ -185,6 +187,47 @@ a group.
Deploy keys can be shared between projects, you just need to add them to each
project.
+### Global shared deploy keys
+
+Global Shared Deploy keys allow read-only or read-write (if enabled) access to
+be configured on any repository in the entire GitLab installation.
+
+This is really useful for integrating repositories to secured, shared Continuous
+Integration (CI) services or other shared services.
+GitLab administrators can set up the Global Shared Deploy key in GitLab and
+add the private key to any shared systems. Individual repositories opt into
+exposing their repsitory using these keys when a project masters (or higher)
+authorizes a Global Shared Deploy key to be used with their project.
+
+Global Shared Keys can provide greater security compared to Per-Project Deploy
+Keys since an administrator of the target integrated system is the only one
+who needs to know and configure the private key.
+
+GitLab administrators set up Global Deploy keys in the Admin area under the
+section **Deploy Keys**. Ensure keys have a meaningful title as that will be
+the primary way for project masters and owners to identify the correct Global
+Deploy key to add. For instance, if the key gives access to a SaaS CI instance,
+use the name of that service in the key name if that is all it is used for.
+When creating Global Shared Deploy keys, give some thought to the granularity
+of keys - they could be of very narrow usage such as just a specific service or
+of broader usage for something like "Anywhere you need to give read access to
+your repository".
+
+Once a GitLab administrator adds the Global Deployment key, project masters
+and owners can add it in project's **Settings > Repository** section by expanding the
+**Deploy Key** section and clicking **Enable** next to the appropriate key listed
+under **Public deploy keys available to any project**.
+
+NOTE: **Note:**
+The heading **Public deploy keys available to any project** only appears
+if there is at least one Global Deploy Key configured.
+
+CAUTION: **Warning:**
+Defining Global Deploy Keys does not expose any given repository via
+the key until that respository adds the Global Deploy Key to their project.
+In this way the Global Deploy Keys enable access by other systems, but do
+not implicitly give any access just by setting them up.
+
## Applications
### Eclipse
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index ea7b1c9a0ed..650d60f1585 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -36,12 +36,16 @@ GFM honors the markdown specification in how [paragraphs and line breaks are han
A paragraph is simply one or more consecutive lines of text, separated by one or more blank lines.
Line-breaks, or softreturns, are rendered if you end a line with two or more spaces:
- Roses are red [followed by two or more spaces]
+[//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
+[//]: # (They are needed for the Markdown text to render correctly.)
+ Roses are red [followed by two or more spaces]
Violets are blue
Sugar is sweet
-Roses are red
+[//]: # (Do *NOT* remove the two ending whitespaces in the following line.)
+[//]: # (They are needed for the Markdown text to render correctly.)
+Roses are red
Violets are blue
Sugar is sweet
diff --git a/doc/user/project/clusters/index.md b/doc/user/project/clusters/index.md
index bbe25c2d911..4ac54f96aa2 100644
--- a/doc/user/project/clusters/index.md
+++ b/doc/user/project/clusters/index.md
@@ -120,6 +120,7 @@ added directly to your configured cluster. Those applications are needed for
| [Helm Tiller](https://docs.helm.sh/) | 10.2+ | Helm is a package manager for Kubernetes and is required to install all the other applications. It will be automatically installed as a dependency when you try to install a different app. It is installed in its own pod inside the cluster which can run the `helm` CLI in a safe environment. |
| [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/) | 10.2+ | Ingress can provide load balancing, SSL termination, and name-based virtual hosting. It acts as a web proxy for your applications and is useful if you want to use [Auto DevOps](../../../topics/autodevops/index.md) or deploy your own web apps. |
| [Prometheus](https://prometheus.io/docs/introduction/overview/) | 10.4+ | Prometheus is an open-source monitoring and alerting system useful to supervise your deployed applications |
+| [GitLab Runner](https://docs.gitlab.com/runner/) | 10.6+ | GitLab Runner is the open source project that is used to run your jobs and send the results back to GitLab. It is used in conjunction with [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), the open-source continuous integration service included with GitLab that coordinates the jobs. |
## Getting the external IP address
diff --git a/doc/user/project/import/img/import_projects_from_repo_url.png b/doc/user/project/import/img/import_projects_from_repo_url.png
new file mode 100644
index 00000000000..ec867da1087
--- /dev/null
+++ b/doc/user/project/import/img/import_projects_from_repo_url.png
Binary files differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index e2b285678c3..72cc58546b7 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -10,6 +10,7 @@
1. [From Perforce](perforce.md)
1. [From SVN](svn.md)
1. [From TFS](tfs.md)
+1. [From repo by URL](repo_by_url.md)
In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
diff --git a/doc/user/project/import/perforce.md b/doc/user/project/import/perforce.md
index aa7508e1e8e..a1ea716b606 100644
--- a/doc/user/project/import/perforce.md
+++ b/doc/user/project/import/perforce.md
@@ -48,3 +48,9 @@ Here's a few links to get you started:
- [git-p4 manual page](https://www.kernel.org/pub/software/scm/git/docs/git-p4.html)
- [git-p4 example usage](https://git.wiki.kernel.org/index.php/Git-p4_Usage)
- [Git book migration guide](https://git-scm.com/book/en/v2/Git-and-Other-Systems-Migrating-to-Git#_perforce_import)
+
+Note that `git p4` and `git filter-branch` are not very good at
+creating small and efficient Git pack files. So it might be a good
+idea to spend time and CPU to properly repack your repository before
+sending it for the first time to your GitLab server. See
+[this StackOverflow question](https://stackoverflow.com/questions/28720151/git-gc-aggressive-vs-git-repack/).
diff --git a/doc/user/project/import/repo_by_url.md b/doc/user/project/import/repo_by_url.md
new file mode 100644
index 00000000000..f43e384de88
--- /dev/null
+++ b/doc/user/project/import/repo_by_url.md
@@ -0,0 +1,12 @@
+# Import project from repo by URL
+
+You can import your existing repositories by providing the Git URL:
+
+1. From your GitLab dashboard click **New project**
+1. Switch to the **Import project** tab
+1. Click on the **Repo by URL** button
+1. Fill in the "Git repository URL" and the remaining project fields
+1. Click **Create project** to being the import process
+1. Once complete, you will be redirected to your newly created project
+
+![Import project by repo URL](img/import_projects_from_repo_url.png)
diff --git a/doc/user/project/integrations/prometheus_library/kubernetes.md b/doc/user/project/integrations/prometheus_library/kubernetes.md
index 02adc562028..8ac753c07bf 100644
--- a/doc/user/project/integrations/prometheus_library/kubernetes.md
+++ b/doc/user/project/integrations/prometheus_library/kubernetes.md
@@ -13,21 +13,18 @@ integration services must be enabled.
| Name | Query |
| ---- | ----- |
-| Average Memory Usage (MB) | (sum(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job))) / count(avg(container_memory_usage_bytes{container_name!="POD",environment="%{ci_environment_slug}"}) without (job)) /1024/1024 |
-| Average CPU Utilization (%) | sum(avg(rate(container_cpu_usage_seconds_total{container_name!="POD",environment="%{ci_environment_slug}"}[2m])) without (job)) * 100 |
+| Average Memory Usage (MB) | avg(sum(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) by (job)) without (job) / count(avg(container_memory_usage_bytes{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}) without (job)) /1024/1024 |
+| Average CPU Utilization (%) | avg(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (job)) without (job) / count(sum(rate(container_cpu_usage_seconds_total{container_name!="POD",pod_name=~"^%{ci_environment_slug}-([^c].*|c([^a]|a([^n]|n([^a]|a([^r]|r[^y])))).*|)-(.*)",namespace="%{kube_namespace}"}[15m])) by (pod_name)) |
-## Configuring Prometheus to monitor for Kubernetes node metrics
+## Configuring Prometheus to monitor for Kubernetes metrics
-In order for Prometheus to collect Kubernetes metrics, you first must have a
-Prometheus server up and running. You have two options here:
+Prometheus needs to be deployed into the cluster and configured properly in order to gather Kubernetes metrics. GitLab supports two methods for doing so:
-- If you have an Omnibus based GitLab installation within your Kubernetes cluster, you can leverage the bundled Prometheus server to [monitor Kubernetes](../../../../administration/monitoring/prometheus/index.md#configuring-prometheus-to-monitor-kubernetes).
-- To configure your own Prometheus server, you can follow the [Prometheus documentation](https://prometheus.io/docs/introduction/overview/) or [our guide](../../../../administration/monitoring/prometheus/index.md#configuring-your-own-prometheus-server-within-kubernetes).
+- GitLab [integrates with Kubernetes](../../clusters/index.md), and can [deploy Prometheus into a connected cluster](../prometheus.html#managed-prometheus-on-kubernetes). It is automatically configured to collect Kubernetes metrics.
+- To configure your own Prometheus server, you can follow the [Prometheus documentation](https://prometheus.io/docs/introduction/overview/).
## Specifying the Environment
In order to isolate and only display relevant CPU and Memory metrics for a given environment, GitLab needs a method to detect which containers it is running. Because these metrics are tracked at the container level, traditional Kubernetes labels are not available.
Instead, the [Deployment](https://kubernetes.io/docs/concepts/workloads/controllers/deployment/) or [DaemonSet](https://kubernetes.io/docs/concepts/workloads/controllers/daemonset/) name should begin with [CI_ENVIRONMENT_SLUG](../../../../ci/variables/README.md#predefined-variables-environment-variables). It can be followed by a `-` and additional content if desired. For example, a deployment name of `review-homepage-5620p5` would match the `review/homepage` environment.
-
-If you are using [GitLab Auto-Deploy](../../../../ci/autodeploy/index.md) and one of the two [provided Kubernetes monitoring solutions](../prometheus.md#getting-started-with-prometheus-monitoring), the `environment` label will be automatically added.
diff --git a/doc/user/project/issues/create_new_issue.md b/doc/user/project/issues/create_new_issue.md
index 9af088374a1..1688edc1ee2 100644
--- a/doc/user/project/issues/create_new_issue.md
+++ b/doc/user/project/issues/create_new_issue.md
@@ -36,3 +36,25 @@ From an Issue Board, create a new issue by clicking on the plus sign (**+**) on
It opens a new issue for that project labeled after its respective list.
![From the issue board](img/new_issue_from_issue_board.png)
+
+## New issue via email
+
+*This feature needs [incoming email](../../../administration/incoming_email.md)
+to be configured by a GitLab administrator to be available for CE/EE users, and
+it's available on GitLab.com.*
+
+At the bottom of a project's issue page, click
+**Email a new issue to this project**, and you will find an email address
+which belongs to you. You could add this address to your contact.
+
+This is a private email address, generated just for you.
+**Keep it to yourself** as anyone who gets ahold of it can create issues or
+merge requests as if they were you. You can add this address to your contact
+list for easy access.
+
+Sending an email to this address will create a new issue on your behalf for
+this project, where the email subject becomes the issue title, and the email
+body becomes the issue description. [Markdown] and [quick actions] are
+supported.
+
+![Bottom of a project issues page](img/new_issue_from_email.png)
diff --git a/doc/user/project/issues/img/new_issue_from_email.png b/doc/user/project/issues/img/new_issue_from_email.png
new file mode 100644
index 00000000000..775ea0cdffb
--- /dev/null
+++ b/doc/user/project/issues/img/new_issue_from_email.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 0de89f90e21..d3220598933 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -34,7 +34,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
- View the deployment process across projects with [Multi-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html#multi-project-pipeline-graphs) (available only in GitLab Premium)
- Request [approvals](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html) from your managers (available in GitLab Starter)
- [Squash and merge](https://docs.gitlab.com/ee/user/project/merge_requests/squash_and_merge.html) for a cleaner commit history (available in GitLab Starter)
-- Analise the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
+- Analyze the impact of your changes with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality_diff.html) (available in GitLab Starter)
## Use cases
@@ -134,6 +134,10 @@ those conflicts in the GitLab UI.
## Create new merge requests by email
+*This feature needs [incoming email](../../../administration/incoming_email.md)
+to be configured by a GitLab administrator to be available for CE/EE users, and
+it's available on GitLab.com.*
+
You can create a new merge request by sending an email to a user-specific email
address. The address can be obtained on the merge requests page by clicking on
a **Email a new merge request to this project** button. The subject will be
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index dedf102fc37..5ddeb014b30 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -31,7 +31,8 @@ with all their related data and be moved into a new GitLab instance.
| GitLab version | Import/Export version |
| ---------------- | --------------------- |
-| 10.4 to current | 0.2.2 |
+| 10.6 to current | 0.2.3 |
+| 10.4 | 0.2.2 |
| 10.3 | 0.2.1 |
| 10.0 | 0.2.0 |
| 9.4.0 | 0.1.8 |
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 60ae5e6b9a2..ae13c248171 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -53,7 +53,10 @@ module API
put ':id/access_requests/:user_id/approve' do
source = find_source(source_type, params[:id])
- member = ::Members::ApproveAccessRequestService.new(source, current_user, declared_params).execute
+ access_requester = source.requesters.find_by!(user_id: params[:user_id])
+ member = ::Members::ApproveAccessRequestService
+ .new(current_user, declared_params)
+ .execute(access_requester)
status :created
present member, with: Entities::Member
@@ -70,8 +73,7 @@ module API
member = source.requesters.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
- ::Members::DestroyService.new(source, current_user, params)
- .execute(:requesters)
+ ::Members::DestroyService.new(current_user).execute(member)
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 754549f72f0..b1b247b70b9 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -108,6 +108,7 @@ module API
mount ::API::AccessRequests
mount ::API::Applications
mount ::API::AwardEmoji
+ mount ::API::Badges
mount ::API::Boards
mount ::API::Branches
mount ::API::BroadcastMessages
diff --git a/lib/api/badges.rb b/lib/api/badges.rb
new file mode 100644
index 00000000000..334948b2995
--- /dev/null
+++ b/lib/api/badges.rb
@@ -0,0 +1,134 @@
+module API
+ class Badges < Grape::API
+ include PaginationParams
+
+ before { authenticate_non_get! }
+
+ helpers ::API::Helpers::BadgesHelpers
+
+ helpers do
+ def find_source_if_admin(source_type)
+ source = find_source(source_type, params[:id])
+
+ authorize_admin_source!(source_type, source)
+
+ source
+ end
+ end
+
+ %w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The ID of a #{source_type}"
+ end
+ resource source_type.pluralize, requirements: API::PROJECT_ENDPOINT_REQUIREMENTS do
+ desc "Gets a list of #{source_type} badges viewable by the authenticated user." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ use :pagination
+ end
+ get ":id/badges" do
+ source = find_source(source_type, params[:id])
+
+ present_badges(source, paginate(source.badges))
+ end
+
+ desc "Preview a badge from a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::BasicBadgeDetails
+ end
+ params do
+ requires :link_url, type: String, desc: 'URL of the badge link'
+ requires :image_url, type: String, desc: 'URL of the badge image'
+ end
+ get ":id/badges/render" do
+ authenticate!
+
+ source = find_source_if_admin(source_type)
+
+ badge = ::Badges::BuildService.new(declared_params(include_missing: false))
+ .execute(source)
+
+ if badge.valid?
+ present_badges(source, badge, with: Entities::BasicBadgeDetails)
+ else
+ render_validation_error!(badge)
+ end
+ end
+
+ desc "Gets a badge of a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ requires :badge_id, type: Integer, desc: 'The badge ID'
+ end
+ get ":id/badges/:badge_id" do
+ source = find_source(source_type, params[:id])
+ badge = find_badge(source)
+
+ present_badges(source, badge)
+ end
+
+ desc "Adds a badge to a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ requires :link_url, type: String, desc: 'URL of the badge link'
+ requires :image_url, type: String, desc: 'URL of the badge image'
+ end
+ post ":id/badges" do
+ source = find_source_if_admin(source_type)
+
+ badge = ::Badges::CreateService.new(declared_params(include_missing: false)).execute(source)
+
+ if badge.persisted?
+ present_badges(source, badge)
+ else
+ render_validation_error!(badge)
+ end
+ end
+
+ desc "Updates a badge of a #{source_type}." do
+ detail 'This feature was introduced in GitLab 10.6.'
+ success Entities::Badge
+ end
+ params do
+ optional :link_url, type: String, desc: 'URL of the badge link'
+ optional :image_url, type: String, desc: 'URL of the badge image'
+ end
+ put ":id/badges/:badge_id" do
+ source = find_source_if_admin(source_type)
+
+ badge = ::Badges::UpdateService.new(declared_params(include_missing: false))
+ .execute(find_badge(source))
+
+ if badge.valid?
+ present_badges(source, badge)
+ else
+ render_validation_error!(badge)
+ end
+ end
+
+ desc 'Removes a badge from a project or group.' do
+ detail 'This feature was introduced in GitLab 10.6.'
+ end
+ params do
+ requires :badge_id, type: Integer, desc: 'The badge ID'
+ end
+ delete ":id/badges/:badge_id" do
+ source = find_source_if_admin(source_type)
+ badge = find_badge(source)
+
+ if badge.is_a?(GroupBadge) && source.is_a?(Project)
+ error!('To delete a Group badge please use the Group endpoint', 403)
+ end
+
+ destroy_conditionally!(badge)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/branches.rb b/lib/api/branches.rb
index 1794207e29b..13cfba728fa 100644
--- a/lib/api/branches.rb
+++ b/lib/api/branches.rb
@@ -16,6 +16,10 @@ module API
render_api_error!('The branch refname is invalid', 400)
end
end
+
+ params :filter_params do
+ optional :search, type: String, desc: 'Return list of branches matching the search criteria'
+ end
end
params do
@@ -27,15 +31,23 @@ module API
end
params do
use :pagination
+ use :filter_params
end
get ':id/repository/branches' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-ce/issues/42329')
repository = user_project.repository
- branches = ::Kaminari.paginate_array(repository.branches.sort_by(&:name))
+
+ branches = BranchesFinder.new(repository, declared_params(include_missing: false)).execute
+
merged_branch_names = repository.merged_branch_names(branches.map(&:name))
- present paginate(branches), with: Entities::Branch, project: user_project, merged_branch_names: merged_branch_names
+ present(
+ paginate(::Kaminari.paginate_array(branches)),
+ with: Entities::Branch,
+ project: user_project,
+ merged_branch_names: merged_branch_names
+ )
end
resource ':id/repository/branches/:branch', requirements: BRANCH_ENDPOINT_REQUIREMENTS do
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 3d6e78d2d80..982f45425a3 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -18,25 +18,28 @@ module API
optional :since, type: DateTime, desc: 'Only commits after or on this date will be returned'
optional :until, type: DateTime, desc: 'Only commits before or on this date will be returned'
optional :path, type: String, desc: 'The file path'
+ optional :all, type: Boolean, desc: 'Every commit will be returned'
use :pagination
end
get ':id/repository/commits' do
path = params[:path]
before = params[:until]
after = params[:since]
- ref = params[:ref_name] || user_project.try(:default_branch) || 'master'
+ ref = params[:ref_name] || user_project.try(:default_branch) || 'master' unless params[:all]
offset = (params[:page] - 1) * params[:per_page]
+ all = params[:all]
commits = user_project.repository.commits(ref,
path: path,
limit: params[:per_page],
offset: offset,
before: before,
- after: after)
+ after: after,
+ all: all)
commit_count =
- if path || before || after
- user_project.repository.count_commits(ref: ref, path: path, before: before, after: after)
+ if all || path || before || after
+ user_project.repository.count_commits(ref: ref, path: path, before: before, after: after, all: all)
else
# Cacheable commit count.
user_project.repository.commit_count_for_ref(ref)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 167878ba600..e5bcbface6b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -71,7 +71,7 @@ module API
end
class ProjectHook < Hook
- expose :project_id, :issues_events
+ expose :project_id, :issues_events, :confidential_issues_events
expose :note_events, :pipeline_events, :wiki_page_events
expose :job_events
end
@@ -481,6 +481,10 @@ module API
expose :id
end
+ class PipelineBasic < Grape::Entity
+ expose :id, :sha, :ref, :status
+ end
+
class MergeRequestSimple < ProjectEntity
expose :title
expose :web_url do |merge_request, options|
@@ -546,6 +550,42 @@ module API
expose :changes_count do |merge_request, _options|
merge_request.merge_request_diff.real_size
end
+
+ expose :merged_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.merged_by
+ end
+
+ expose :merged_at do |merge_request, _options|
+ merge_request.metrics&.merged_at
+ end
+
+ expose :closed_by, using: Entities::UserBasic do |merge_request, _options|
+ merge_request.metrics&.latest_closed_by
+ end
+
+ expose :closed_at do |merge_request, _options|
+ merge_request.metrics&.latest_closed_at
+ end
+
+ expose :latest_build_started_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.latest_build_started_at
+ end
+
+ expose :latest_build_finished_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.latest_build_finished_at
+ end
+
+ expose :first_deployed_to_production_at, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.first_deployed_to_production_at
+ end
+
+ expose :pipeline, using: Entities::PipelineBasic, if: -> (_, options) { build_available?(options) } do |merge_request, _options|
+ merge_request.metrics&.pipeline
+ end
+
+ def build_available?(options)
+ options[:project]&.feature_available?(:builds, options[:current_user])
+ end
end
class MergeRequestChanges < MergeRequest
@@ -909,10 +949,6 @@ module API
expose :filename, :size
end
- class PipelineBasic < Grape::Entity
- expose :id, :sha, :ref, :status
- end
-
class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
@@ -1199,5 +1235,23 @@ module API
expose :startline
expose :project_id
end
+
+ class BasicBadgeDetails < Grape::Entity
+ expose :link_url
+ expose :image_url
+ expose :rendered_link_url do |badge, options|
+ badge.rendered_link_url(options.fetch(:project, nil))
+ end
+ expose :rendered_image_url do |badge, options|
+ badge.rendered_image_url(options.fetch(:project, nil))
+ end
+ end
+
+ class Badge < BasicBadgeDetails
+ expose :id
+ expose :kind do |badge|
+ badge.type == 'ProjectBadge' ? 'project' : 'group'
+ end
+ end
end
end
diff --git a/lib/api/helpers/badges_helpers.rb b/lib/api/helpers/badges_helpers.rb
new file mode 100644
index 00000000000..1f8afbf3c90
--- /dev/null
+++ b/lib/api/helpers/badges_helpers.rb
@@ -0,0 +1,28 @@
+module API
+ module Helpers
+ module BadgesHelpers
+ include ::API::Helpers::MembersHelpers
+
+ def find_badge(source)
+ source.badges.find(params[:badge_id])
+ end
+
+ def present_badges(source, records, options = {})
+ entity_type = options[:with] || Entities::Badge
+ badge_params = badge_source_params(source).merge(with: entity_type)
+
+ present records, badge_params
+ end
+
+ def badge_source_params(source)
+ project = if source.is_a?(Project)
+ source
+ else
+ GroupProjectsFinder.new(group: source, current_user: current_user).execute.first
+ end
+
+ { project: project }
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers/runner.rb b/lib/api/helpers/runner.rb
index fbe30192a16..35ac0b4cbca 100644
--- a/lib/api/helpers/runner.rb
+++ b/lib/api/helpers/runner.rb
@@ -9,16 +9,22 @@ module API
Gitlab::CurrentSettings.runners_registration_token)
end
- def get_runner_version_from_params
- return unless params['info'].present?
+ def authenticate_runner!
+ forbidden! unless current_runner
- attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ current_runner
+ .update_cached_info(get_runner_details_from_request)
end
- def authenticate_runner!
- forbidden! unless current_runner
+ def get_runner_details_from_request
+ return get_runner_ip unless params['info'].present?
+
+ attributes_for_keys(%w(name version revision platform architecture), params['info'])
+ .merge(get_runner_ip)
+ end
- current_runner.update_cached_info(get_runner_version_from_params)
+ def get_runner_ip
+ { ip_address: request.ip }
end
def current_runner
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index b6c278c89d0..f74b3b26802 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -32,6 +32,8 @@ module API
optional :search, type: String, desc: 'Search issues for text present in the title or description'
optional :created_after, type: DateTime, desc: 'Return issues created after the specified time'
optional :created_before, type: DateTime, desc: 'Return issues created before the specified time'
+ optional :updated_after, type: DateTime, desc: 'Return issues updated after the specified time'
+ optional :updated_before, type: DateTime, desc: 'Return issues updated before the specified time'
optional :author_id, type: Integer, desc: 'Return issues which are authored by the user with the given ID'
optional :assignee_id, type: Integer, desc: 'Return issues which are assigned to the user with the given ID'
optional :scope, type: String, values: %w[created-by-me assigned-to-me all],
diff --git a/lib/api/members.rb b/lib/api/members.rb
index bc1de37284a..8b12986d09e 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -81,12 +81,16 @@ module API
source = find_source(source_type, params.delete(:id))
authorize_admin_source!(source_type, source)
- member = source.members.find_by!(user_id: params.delete(:user_id))
+ member = source.members.find_by!(user_id: params[:user_id])
+ updated_member =
+ ::Members::UpdateService
+ .new(current_user, declared_params(include_missing: false))
+ .execute(member)
- if member.update_attributes(declared_params(include_missing: false))
- present member, with: Entities::Member
+ if updated_member.valid?
+ present updated_member, with: Entities::Member
else
- render_validation_error!(member)
+ render_validation_error!(updated_member)
end
end
@@ -99,7 +103,7 @@ module API
member = source.members.find_by!(user_id: params[:user_id])
destroy_conditionally!(member) do
- ::Members::DestroyService.new(source, current_user, declared_params).execute
+ ::Members::DestroyService.new(current_user).execute(member)
end
end
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 719afa09295..8c02972b421 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -42,12 +42,16 @@ module API
optional :labels, type: String, desc: 'Comma-separated list of label names'
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time'
+ optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time'
+ optional :updated_before, type: DateTime, desc: 'Return merge requests updated before the specified time'
optional :view, type: String, values: %w[simple], desc: 'If simple, returns the `iid`, URL, title, description, and basic state of merge request'
optional :author_id, type: Integer, desc: 'Return merge requests which are authored by the user with the given ID'
optional :assignee_id, type: Integer, desc: 'Return merge requests which are assigned to the user with the given ID'
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 :source_branch, type: String, desc: 'Return merge requests with the given source branch'
+ optional :target_branch, type: String, desc: 'Return merge requests with the given target branch'
optional :search, type: String, desc: 'Search merge requests for text present in the title or description'
use :pagination
end
@@ -220,7 +224,7 @@ module API
get ':id/merge_requests/:merge_request_iid/changes' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present merge_request, with: Entities::MergeRequestChanges, current_user: current_user
+ present merge_request, with: Entities::MergeRequestChanges, current_user: current_user, project: user_project
end
desc 'Get the merge request pipelines' do
diff --git a/lib/api/project_hooks.rb b/lib/api/project_hooks.rb
index 86066e2b58f..f82241058e5 100644
--- a/lib/api/project_hooks.rb
+++ b/lib/api/project_hooks.rb
@@ -10,6 +10,7 @@ module API
requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 5469cba69a6..7e6c33ec33d 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -16,7 +16,8 @@ module API
optional :tag_list, type: Array[String], desc: %q(List of Runner's tags)
end
post '/' do
- attributes = attributes_for_keys [:description, :locked, :run_untagged, :tag_list]
+ attributes = attributes_for_keys([:description, :locked, :run_untagged, :tag_list])
+ .merge(get_runner_details_from_request)
runner =
if runner_registration_token_valid?
@@ -30,7 +31,6 @@ module API
return forbidden! unless runner
if runner.id
- runner.update(get_runner_version_from_params)
present runner, with: Entities::RunnerRegistrationDetails
else
not_found!
@@ -204,6 +204,7 @@ module API
optional 'file.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
optional 'file.name', type: String, desc: %q(real filename as send in Content-Disposition (generated by Workhorse))
optional 'file.type', type: String, desc: %q(real content type as send in Content-Type (generated by Workhorse))
+ optional 'file.sha256', type: String, desc: %q(sha256 checksum of the file)
optional 'metadata.path', type: String, desc: %q(path to locally stored body (generated by Workhorse))
optional 'metadata.name', type: String, desc: %q(filename (generated by Workhorse))
end
@@ -224,7 +225,7 @@ module API
expire_in = params['expire_in'] ||
Gitlab::CurrentSettings.current_application_settings.default_artifacts_expire_in
- job.build_job_artifacts_archive(project: job.project, file_type: :archive, file: artifacts, expire_in: expire_in)
+ job.build_job_artifacts_archive(project: job.project, file_type: :archive, file: artifacts, file_sha256: params['file.sha256'], expire_in: expire_in)
job.build_job_artifacts_metadata(project: job.project, file_type: :metadata, file: metadata, expire_in: expire_in) if metadata
job.artifacts_expire_in = expire_in
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 51e33e2c686..6c97659166d 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -1,6 +1,7 @@
+# frozen_string_literal: true
module API
class Services < Grape::API
- chat_notification_settings = [
+ CHAT_NOTIFICATION_SETTINGS = [
{
required: true,
name: :webhook,
@@ -19,9 +20,9 @@ module API
type: String,
desc: 'The default chat channel'
}
- ]
+ ].freeze
- chat_notification_flags = [
+ CHAT_NOTIFICATION_FLAGS = [
{
required: false,
name: :notify_only_broken_pipelines,
@@ -34,9 +35,9 @@ module API
type: Boolean,
desc: 'Send notifications only for the default branch'
}
- ]
+ ].freeze
- chat_notification_channels = [
+ CHAT_NOTIFICATION_CHANNELS = [
{
required: false,
name: :push_channel,
@@ -85,9 +86,9 @@ module API
type: String,
desc: 'The name of the channel to receive wiki_page_events notifications'
}
- ]
+ ].freeze
- chat_notification_events = [
+ CHAT_NOTIFICATION_EVENTS = [
{
required: false,
name: :push_events,
@@ -136,7 +137,7 @@ module API
type: Boolean,
desc: 'Enable notifications for wiki_page_events'
}
- ]
+ ].freeze
services = {
'asana' => [
@@ -627,10 +628,10 @@ module API
}
],
'slack' => [
- chat_notification_settings,
- chat_notification_flags,
- chat_notification_channels,
- chat_notification_events
+ CHAT_NOTIFICATION_SETTINGS,
+ CHAT_NOTIFICATION_FLAGS,
+ CHAT_NOTIFICATION_CHANNELS,
+ CHAT_NOTIFICATION_EVENTS
].flatten,
'microsoft-teams' => [
{
@@ -641,10 +642,10 @@ module API
}
],
'mattermost' => [
- chat_notification_settings,
- chat_notification_flags,
- chat_notification_channels,
- chat_notification_events
+ CHAT_NOTIFICATION_SETTINGS,
+ CHAT_NOTIFICATION_FLAGS,
+ CHAT_NOTIFICATION_CHANNELS,
+ CHAT_NOTIFICATION_EVENTS
].flatten,
'teamcity' => [
{
@@ -724,7 +725,22 @@ module API
]
end
- trigger_services = {
+ SERVICES = services.freeze
+ SERVICE_CLASSES = service_classes.freeze
+
+ SERVICE_CLASSES.each do |service|
+ event_names = service.try(:event_names) || next
+ event_names.each do |event_name|
+ SERVICES[service.to_param.tr("_", "-")] << {
+ required: false,
+ name: event_name.to_sym,
+ type: String,
+ desc: ServicesHelper.service_event_description(event_name)
+ }
+ end
+ end
+
+ TRIGGER_SERVICES = {
'mattermost-slash-commands' => [
{
name: :token,
@@ -756,22 +772,9 @@ module API
end
end
- services.each do |service_slug, settings|
+ SERVICES.each do |service_slug, settings|
desc "Set #{service_slug} service for project"
params do
- service_classes.each do |service|
- event_names = service.try(:event_names) || next
- event_names.each do |event_name|
- services[service.to_param.tr("_", "-")] << {
- required: false,
- name: event_name.to_sym,
- type: String,
- desc: ServicesHelper.service_event_description(event_name)
- }
- end
- end
- services.freeze
-
settings.each do |setting|
if setting[:required]
requires setting[:name], type: setting[:type], desc: setting[:desc]
@@ -794,7 +797,7 @@ module API
desc "Delete a service for project"
params do
- requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end
delete ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
@@ -814,7 +817,7 @@ module API
success Entities::ProjectService
end
params do
- requires :service_slug, type: String, values: services.keys, desc: 'The name of the service'
+ requires :service_slug, type: String, values: SERVICES.keys, desc: 'The name of the service'
end
get ":id/services/:service_slug" do
service = user_project.find_or_initialize_service(params[:service_slug].underscore)
@@ -822,7 +825,7 @@ module API
end
end
- trigger_services.each do |service_slug, settings|
+ TRIGGER_SERVICES.each do |service_slug, settings|
helpers do
def slash_command_service(project, service_slug, params)
project.services.active.where(template: false).find do |service|
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 2ccbb9da1c5..68b4d7c3982 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -252,8 +252,9 @@ module API
class ProjectService < Grape::Entity
expose :id, :title, :created_at, :updated_at, :active
- expose :push_events, :issues_events, :merge_requests_events
- expose :tag_push_events, :note_events, :pipeline_events
+ expose :push_events, :issues_events, :confidential_issues_events
+ expose :merge_requests_events, :tag_push_events, :note_events
+ expose :pipeline_events
expose :job_events, as: :build_events
# Expose serialized properties
expose :properties do |service, options|
@@ -262,8 +263,9 @@ module API
end
class ProjectHook < ::API::Entities::Hook
- expose :project_id, :issues_events, :merge_requests_events
- expose :note_events, :pipeline_events, :wiki_page_events
+ expose :project_id, :issues_events, :confidential_issues_events
+ expose :merge_requests_events, :note_events, :pipeline_events
+ expose :wiki_page_events
expose :job_events, as: :build_events
end
diff --git a/lib/api/v3/members.rb b/lib/api/v3/members.rb
index d7bde8ceb89..88dd598f1e9 100644
--- a/lib/api/v3/members.rb
+++ b/lib/api/v3/members.rb
@@ -124,7 +124,7 @@ module API
status(200 )
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(source, current_user, declared_params).execute
+ ::Members::DestroyService.new(current_user).execute(member)
present member, with: ::API::Entities::Member
end
diff --git a/lib/api/v3/project_hooks.rb b/lib/api/v3/project_hooks.rb
index 51014591a93..631944150c7 100644
--- a/lib/api/v3/project_hooks.rb
+++ b/lib/api/v3/project_hooks.rb
@@ -11,6 +11,7 @@ module API
requires :url, type: String, desc: "The URL to send the request to"
optional :push_events, type: Boolean, desc: "Trigger hook on push events"
optional :issues_events, type: Boolean, desc: "Trigger hook on issues events"
+ optional :confidential_issues_events, type: Boolean, desc: "Trigger hook on confidential issues events"
optional :merge_requests_events, type: Boolean, desc: "Trigger hook on merge request events"
optional :tag_push_events, type: Boolean, desc: "Trigger hook on tag push events"
optional :note_events, type: Boolean, desc: "Trigger hook on note(comment) events"
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index e7e6a90b5fd..c9e3f8ce42b 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -174,7 +174,9 @@ module Banzai
title = object_link_title(object)
klass = reference_class(object_sym)
- data = data_attributes_for(link_content || match, parent, object, link: !!link_content)
+ data = data_attributes_for(link_content || match, parent, object,
+ link_content: !!link_content,
+ link_reference: link_reference)
url =
if matches.names.include?("url") && matches[:url]
@@ -194,12 +196,13 @@ module Banzai
end
end
- def data_attributes_for(text, project, object, link: false)
+ def data_attributes_for(text, project, object, link_content: false, link_reference: false)
data_attribute(
- original: text,
- link: link,
- project: project.id,
- object_sym => object.id
+ original: text,
+ link: link_content,
+ link_reference: link_reference,
+ project: project.id,
+ object_sym => object.id
)
end
diff --git a/lib/banzai/filter/autolink_filter.rb b/lib/banzai/filter/autolink_filter.rb
index b8d2673c1a6..75b64ae9af2 100644
--- a/lib/banzai/filter/autolink_filter.rb
+++ b/lib/banzai/filter/autolink_filter.rb
@@ -25,8 +25,8 @@ module Banzai
# period or comma for punctuation without those characters being included
# in the generated link.
#
- # Rubular: http://rubular.com/r/cxjPyZc7Sb
- LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://\S+)(?<!,|\.)}
+ # Rubular: http://rubular.com/r/JzPhi6DCZp
+ LINK_PATTERN = %r{([a-z][a-z0-9\+\.-]+://[^\s>]+)(?<!,|\.)}
# Text matching LINK_PATTERN inside these elements will not be linked
IGNORE_PARENTS = %w(a code kbd pre script style).to_set
@@ -35,53 +35,19 @@ module Banzai
TEXT_QUERY = %Q(descendant-or-self::text()[
not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
and contains(., '://')
- and not(starts-with(., 'http'))
- and not(starts-with(., 'ftp'))
]).freeze
+ PUNCTUATION_PAIRS = {
+ "'" => "'",
+ '"' => '"',
+ ')' => '(',
+ ']' => '[',
+ '}' => '{'
+ }.freeze
+
def call
return doc if context[:autolink] == false
- rinku_parse
- text_parse
- end
-
- private
-
- # Run the text through Rinku as a first pass
- #
- # This will quickly autolink http(s) and ftp links.
- #
- # `@doc` will be re-parsed with the HTML String from Rinku.
- def rinku_parse
- # Convert the options from a Hash to a String that Rinku expects
- options = tag_options(link_options)
-
- # NOTE: We don't parse email links because it will erroneously match
- # external Commit and CommitRange references.
- #
- # The final argument tells Rinku to link short URLs that don't include a
- # period (e.g., http://localhost:3000/)
- rinku = Rinku.auto_link(html, :urls, options, IGNORE_PARENTS.to_a, 1)
-
- return if rinku == html
-
- # Rinku returns a String, so parse it back to a Nokogiri::XML::Document
- # for further processing.
- @doc = parse_html(rinku)
- end
-
- # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme
- def contains_unsafe?(scheme)
- return false unless scheme
-
- scheme = scheme.strip.downcase
- Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) }
- end
-
- # Autolinks any text matching LINK_PATTERN that Rinku didn't already
- # replace
- def text_parse
doc.xpath(TEXT_QUERY).each do |node|
content = node.to_html
@@ -97,6 +63,16 @@ module Banzai
doc
end
+ private
+
+ # Return true if any of the UNSAFE_PROTOCOLS strings are included in the URI scheme
+ def contains_unsafe?(scheme)
+ return false unless scheme
+
+ scheme = scheme.strip.downcase
+ Banzai::Filter::SanitizationFilter::UNSAFE_PROTOCOLS.any? { |protocol| scheme.include?(protocol) }
+ end
+
def autolink_match(match)
# start by stripping out dangerous links
begin
@@ -112,12 +88,30 @@ module Banzai
match.gsub!(/((?:&[\w#]+;)+)\z/, '')
dropped = ($1 || '').html_safe
+ # To match the behaviour of Rinku, if the matched link ends with a
+ # closing part of a matched pair of punctuation, we remove that trailing
+ # character unless there are an equal number of closing and opening
+ # characters in the link.
+ if match.end_with?(*PUNCTUATION_PAIRS.keys)
+ close_character = match[-1]
+ close_count = match.count(close_character)
+ open_character = PUNCTUATION_PAIRS[close_character]
+ open_count = match.count(open_character)
+
+ if open_count != close_count || open_character == close_character
+ dropped += close_character
+ match = match[0..-2]
+ end
+ end
+
options = link_options.merge(href: match)
- content_tag(:a, match, options) + dropped
+ content_tag(:a, match.html_safe, options) + dropped
end
def autolink_filter(text)
- text.gsub(LINK_PATTERN) { |match| autolink_match(match) }
+ Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:|
+ autolink_match(link)
+ end
end
def link_options
diff --git a/lib/banzai/redactor.rb b/lib/banzai/redactor.rb
index 827df7c08ae..fd457bebf03 100644
--- a/lib/banzai/redactor.rb
+++ b/lib/banzai/redactor.rb
@@ -42,16 +42,33 @@ module Banzai
next if visible.include?(node)
doc_data[:visible_reference_count] -= 1
- # The reference should be replaced by the original link's content,
- # which is not always the same as the rendered one.
- content = node.attr('data-original') || node.inner_html
- node.replace(content)
+ redacted_content = redacted_node_content(node)
+ node.replace(redacted_content)
end
end
metadata
end
+ # Return redacted content of given node as either the original link (<a> tag),
+ # the original content (text), or the inner HTML of the node.
+ #
+ def redacted_node_content(node)
+ original_content = node.attr('data-original')
+ link_reference = node.attr('data-link-reference')
+
+ # Build the raw <a> tag just with a link as href and content if
+ # it's originally a link pattern. We shouldn't return a plain text href.
+ original_link =
+ if link_reference == 'true' && href = original_content
+ %(<a href="#{href}">#{href}</a>)
+ end
+
+ # The reference should be replaced by the original link's content,
+ # which is not always the same as the rendered one.
+ original_link || original_content || node.inner_html
+ end
+
def redact_cross_project_references(documents)
extractor = Banzai::IssuableExtractor.new(project, user)
issuables = extractor.extract(documents)
diff --git a/lib/bitbucket/connection.rb b/lib/bitbucket/connection.rb
index b9279c33f5b..ba5a9e2f04c 100644
--- a/lib/bitbucket/connection.rb
+++ b/lib/bitbucket/connection.rb
@@ -57,7 +57,7 @@ module Bitbucket
end
def provider
- Gitlab::OAuth::Provider.config_for('bitbucket')
+ Gitlab::Auth::OAuth::Provider.config_for('bitbucket')
end
def options
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 05932378173..f5ccf952cf9 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -40,8 +40,8 @@ module Gitlab
end
def find_with_user_password(login, password)
- # Avoid resource intensive login checks if password is not provided
- return unless password.present?
+ # Avoid resource intensive checks if login credentials are not provided
+ return unless login.present? && password.present?
# Nothing to do here if internal auth is disabled and LDAP is
# not configured
@@ -50,14 +50,26 @@ module Gitlab
Gitlab::Auth::UniqueIpsLimiter.limit_user! do
user = User.by_login(login)
- # If no user is found, or it's an LDAP server, try LDAP.
- # LDAP users are only authenticated via LDAP
- if user.nil? || user.ldap_user?
- # Second chance - try LDAP authentication
- Gitlab::LDAP::Authentication.login(login, password)
- elsif Gitlab::CurrentSettings.password_authentication_enabled_for_git?
- user if user.active? && user.valid_password?(password)
+ return if user && !user.active?
+
+ authenticators = []
+
+ if user
+ authenticators << Gitlab::Auth::OAuth::Provider.authentication(user, 'database')
+
+ # Add authenticators for all identities if user is not nil
+ user&.identities&.each do |identity|
+ authenticators << Gitlab::Auth::OAuth::Provider.authentication(user, identity.provider)
+ end
+ else
+ # If no user is provided, try LDAP.
+ # LDAP users are only authenticated via LDAP
+ authenticators << Gitlab::Auth::LDAP::Authentication
end
+
+ authenticators.compact!
+
+ user if authenticators.find { |auth| auth.login(login, password) }
end
end
@@ -85,7 +97,7 @@ module Gitlab
private
def authenticate_using_internal_or_ldap_password?
- Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::LDAP::Config.enabled?
+ Gitlab::CurrentSettings.password_authentication_enabled_for_git? || Gitlab::Auth::LDAP::Config.enabled?
end
def service_request_check(login, password, project)
diff --git a/lib/gitlab/auth/database/authentication.rb b/lib/gitlab/auth/database/authentication.rb
new file mode 100644
index 00000000000..260a77058a4
--- /dev/null
+++ b/lib/gitlab/auth/database/authentication.rb
@@ -0,0 +1,16 @@
+# These calls help to authenticate to OAuth provider by providing username and password
+#
+
+module Gitlab
+ module Auth
+ module Database
+ class Authentication < Gitlab::Auth::OAuth::Authentication
+ def login(login, password)
+ return false unless Gitlab::CurrentSettings.password_authentication_enabled_for_git?
+
+ user&.valid_password?(password)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/access.rb b/lib/gitlab/auth/ldap/access.rb
new file mode 100644
index 00000000000..77c0ddc2d48
--- /dev/null
+++ b/lib/gitlab/auth/ldap/access.rb
@@ -0,0 +1,89 @@
+# LDAP authorization model
+#
+# * Check if we are allowed access (not blocked)
+#
+module Gitlab
+ module Auth
+ module LDAP
+ class Access
+ attr_reader :provider, :user
+
+ def self.open(user, &block)
+ Gitlab::Auth::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
+ block.call(self.new(user, adapter))
+ end
+ end
+
+ def self.allowed?(user)
+ self.open(user) do |access|
+ if access.allowed?
+ Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
+
+ true
+ else
+ false
+ end
+ end
+ end
+
+ def initialize(user, adapter = nil)
+ @adapter = adapter
+ @user = user
+ @provider = user.ldap_identity.provider
+ end
+
+ def allowed?
+ if ldap_user
+ unless ldap_config.active_directory
+ unblock_user(user, 'is available again') if user.ldap_blocked?
+ return true
+ end
+
+ # Block user in GitLab if he/she was blocked in AD
+ if Gitlab::Auth::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
+ block_user(user, 'is disabled in Active Directory')
+ false
+ else
+ unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
+ true
+ end
+ else
+ # Block the user if they no longer exist in LDAP/AD
+ block_user(user, 'does not exist anymore')
+ false
+ end
+ end
+
+ def adapter
+ @adapter ||= Gitlab::Auth::LDAP::Adapter.new(provider)
+ end
+
+ def ldap_config
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def ldap_user
+ @ldap_user ||= Gitlab::Auth::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
+ end
+
+ def block_user(user, reason)
+ user.ldap_block
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "blocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+
+ def unblock_user(user, reason)
+ user.activate
+
+ Gitlab::AppLogger.info(
+ "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
+ "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
+ )
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/adapter.rb b/lib/gitlab/auth/ldap/adapter.rb
new file mode 100644
index 00000000000..caf2d18c668
--- /dev/null
+++ b/lib/gitlab/auth/ldap/adapter.rb
@@ -0,0 +1,110 @@
+module Gitlab
+ module Auth
+ module LDAP
+ class Adapter
+ attr_reader :provider, :ldap
+
+ def self.open(provider, &block)
+ Net::LDAP.open(config(provider).adapter_options) do |ldap|
+ block.call(self.new(provider, ldap))
+ end
+ end
+
+ def self.config(provider)
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def initialize(provider, ldap = nil)
+ @provider = provider
+ @ldap = ldap || Net::LDAP.new(config.adapter_options)
+ end
+
+ def config
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def users(fields, value, limit = nil)
+ options = user_options(Array(fields), value, limit)
+
+ entries = ldap_search(options).select do |entry|
+ entry.respond_to? config.uid
+ end
+
+ entries.map do |entry|
+ Gitlab::Auth::LDAP::Person.new(entry, provider)
+ end
+ end
+
+ def user(*args)
+ users(*args).first
+ end
+
+ def dn_matches_filter?(dn, filter)
+ ldap_search(base: dn,
+ filter: filter,
+ scope: Net::LDAP::SearchScope_BaseObject,
+ attributes: %w{dn}).any?
+ end
+
+ def ldap_search(*args)
+ # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
+ Timeout.timeout(config.timeout) do
+ results = ldap.search(*args)
+
+ if results.nil?
+ response = ldap.get_operation_result
+
+ unless response.code.zero?
+ Rails.logger.warn("LDAP search error: #{response.message}")
+ end
+
+ []
+ else
+ results
+ end
+ end
+ rescue Net::LDAP::Error => error
+ Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
+ []
+ rescue Timeout::Error
+ Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
+ []
+ end
+
+ private
+
+ def user_options(fields, value, limit)
+ options = {
+ attributes: Gitlab::Auth::LDAP::Person.ldap_attributes(config),
+ base: config.base
+ }
+
+ options[:size] = limit if limit
+
+ if fields.include?('dn')
+ raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
+
+ options[:base] = value
+ options[:scope] = Net::LDAP::SearchScope_BaseObject
+ else
+ filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
+ end
+
+ options.merge(filter: user_filter(filter))
+ end
+
+ def user_filter(filter = nil)
+ user_filter = config.constructed_user_filter if config.user_filter.present?
+
+ if user_filter && filter
+ Net::LDAP::Filter.join(filter, user_filter)
+ elsif user_filter
+ user_filter
+ else
+ filter
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/auth_hash.rb b/lib/gitlab/auth/ldap/auth_hash.rb
new file mode 100644
index 00000000000..ac5c14d374d
--- /dev/null
+++ b/lib/gitlab/auth/ldap/auth_hash.rb
@@ -0,0 +1,48 @@
+# Class to parse and transform the info provided by omniauth
+#
+module Gitlab
+ module Auth
+ module LDAP
+ class AuthHash < Gitlab::Auth::OAuth::AuthHash
+ def uid
+ @uid ||= Gitlab::Auth::LDAP::Person.normalize_dn(super)
+ end
+
+ def username
+ super.tap do |username|
+ username.downcase! if ldap_config.lowercase_usernames
+ end
+ end
+
+ private
+
+ def get_info(key)
+ attributes = ldap_config.attributes[key.to_s]
+ return super unless attributes
+
+ attributes = Array(attributes)
+
+ value = nil
+ attributes.each do |attribute|
+ value = get_raw(attribute)
+ value = value.first if value
+ break if value.present?
+ end
+
+ return super unless value
+
+ Gitlab::Utils.force_utf8(value)
+ value
+ end
+
+ def get_raw(key)
+ auth_hash.extra[:raw_info][key] if auth_hash.extra
+ end
+
+ def ldap_config
+ @ldap_config ||= Gitlab::Auth::LDAP::Config.new(self.provider)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/authentication.rb b/lib/gitlab/auth/ldap/authentication.rb
new file mode 100644
index 00000000000..e70c3ab6b46
--- /dev/null
+++ b/lib/gitlab/auth/ldap/authentication.rb
@@ -0,0 +1,68 @@
+# These calls help to authenticate to LDAP by providing username and password
+#
+# Since multiple LDAP servers are supported, it will loop through all of them
+# until a valid bind is found
+#
+
+module Gitlab
+ module Auth
+ module LDAP
+ class Authentication < Gitlab::Auth::OAuth::Authentication
+ def self.login(login, password)
+ return unless Gitlab::Auth::LDAP::Config.enabled?
+ return unless login.present? && password.present?
+
+ auth = nil
+ # loop through providers until valid bind
+ providers.find do |provider|
+ auth = new(provider)
+ auth.login(login, password) # true will exit the loop
+ end
+
+ # If (login, password) was invalid for all providers, the value of auth is now the last
+ # Gitlab::Auth::LDAP::Authentication instance we tried.
+ auth.user
+ end
+
+ def self.providers
+ Gitlab::Auth::LDAP::Config.providers
+ end
+
+ attr_accessor :ldap_user
+
+ def login(login, password)
+ @ldap_user = adapter.bind_as(
+ filter: user_filter(login),
+ size: 1,
+ password: password
+ )
+ end
+
+ def adapter
+ OmniAuth::LDAP::Adaptor.new(config.omniauth_options)
+ end
+
+ def config
+ Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ def user_filter(login)
+ filter = Net::LDAP::Filter.equals(config.uid, login)
+
+ # Apply LDAP user filter if present
+ if config.user_filter.present?
+ filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
+ end
+
+ filter
+ end
+
+ def user
+ return unless ldap_user
+
+ Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/config.rb b/lib/gitlab/auth/ldap/config.rb
new file mode 100644
index 00000000000..77185f52ced
--- /dev/null
+++ b/lib/gitlab/auth/ldap/config.rb
@@ -0,0 +1,237 @@
+# Load a specific server configuration
+module Gitlab
+ module Auth
+ module LDAP
+ class Config
+ NET_LDAP_ENCRYPTION_METHOD = {
+ simple_tls: :simple_tls,
+ start_tls: :start_tls,
+ plain: nil
+ }.freeze
+
+ attr_accessor :provider, :options
+
+ def self.enabled?
+ Gitlab.config.ldap.enabled
+ end
+
+ def self.servers
+ Gitlab.config.ldap['servers']&.values || []
+ end
+
+ def self.available_servers
+ return [] unless enabled?
+
+ Array.wrap(servers.first)
+ end
+
+ def self.providers
+ servers.map { |server| server['provider_name'] }
+ end
+
+ def self.valid_provider?(provider)
+ providers.include?(provider)
+ end
+
+ def self.invalid_provider(provider)
+ raise "Unknown provider (#{provider}). Available providers: #{providers}"
+ end
+
+ def initialize(provider)
+ if self.class.valid_provider?(provider)
+ @provider = provider
+ else
+ self.class.invalid_provider(provider)
+ end
+
+ @options = config_for(@provider) # Use @provider, not provider
+ end
+
+ def enabled?
+ base_config.enabled
+ end
+
+ def adapter_options
+ opts = base_options.merge(
+ encryption: encryption_options
+ )
+
+ opts.merge!(auth_options) if has_auth?
+
+ opts
+ end
+
+ def omniauth_options
+ opts = base_options.merge(
+ base: base,
+ encryption: options['encryption'],
+ filter: omniauth_user_filter,
+ name_proc: name_proc,
+ disable_verify_certificates: !options['verify_certificates']
+ )
+
+ if has_auth?
+ opts.merge!(
+ bind_dn: options['bind_dn'],
+ password: options['password']
+ )
+ end
+
+ opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
+ opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
+
+ opts
+ end
+
+ def base
+ options['base']
+ end
+
+ def uid
+ options['uid']
+ end
+
+ def sync_ssh_keys?
+ sync_ssh_keys.present?
+ end
+
+ # The LDAP attribute in which the ssh keys are stored
+ def sync_ssh_keys
+ options['sync_ssh_keys']
+ end
+
+ def user_filter
+ options['user_filter']
+ end
+
+ def constructed_user_filter
+ @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
+ end
+
+ def group_base
+ options['group_base']
+ end
+
+ def admin_group
+ options['admin_group']
+ end
+
+ def active_directory
+ options['active_directory']
+ end
+
+ def block_auto_created_users
+ options['block_auto_created_users']
+ end
+
+ def attributes
+ default_attributes.merge(options['attributes'])
+ end
+
+ def timeout
+ options['timeout'].to_i
+ end
+
+ def has_auth?
+ options['password'] || options['bind_dn']
+ end
+
+ def allow_username_or_email_login
+ options['allow_username_or_email_login']
+ end
+
+ def lowercase_usernames
+ options['lowercase_usernames']
+ end
+
+ def name_proc
+ if allow_username_or_email_login
+ proc { |name| name.gsub(/@.*\z/, '') }
+ else
+ proc { |name| name }
+ end
+ end
+
+ def default_attributes
+ {
+ 'username' => %w(uid sAMAccountName userid),
+ 'email' => %w(mail email userPrincipalName),
+ 'name' => 'cn',
+ 'first_name' => 'givenName',
+ 'last_name' => 'sn'
+ }
+ end
+
+ protected
+
+ def base_options
+ {
+ host: options['host'],
+ port: options['port']
+ }
+ end
+
+ def base_config
+ Gitlab.config.ldap
+ end
+
+ def config_for(provider)
+ base_config.servers.values.find { |server| server['provider_name'] == provider }
+ end
+
+ def encryption_options
+ method = translate_method(options['encryption'])
+ return nil unless method
+
+ {
+ method: method,
+ tls_options: tls_options(method)
+ }
+ end
+
+ def translate_method(method_from_config)
+ NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym]
+ end
+
+ def tls_options(method)
+ return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method
+
+ opts = if options['verify_certificates']
+ OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
+ else
+ # It is important to explicitly set verify_mode for two reasons:
+ # 1. The behavior of OpenSSL is undefined when verify_mode is not set.
+ # 2. The net-ldap gem implementation verifies the certificate hostname
+ # unless verify_mode is set to VERIFY_NONE.
+ { verify_mode: OpenSSL::SSL::VERIFY_NONE }
+ end
+
+ opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
+ opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
+
+ opts
+ end
+
+ def auth_options
+ {
+ auth: {
+ method: :simple,
+ username: options['bind_dn'],
+ password: options['password']
+ }
+ }
+ end
+
+ def omniauth_user_filter
+ uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
+
+ if user_filter.present?
+ Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
+ else
+ uid_filter.to_s
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/dn.rb b/lib/gitlab/auth/ldap/dn.rb
new file mode 100644
index 00000000000..1fa5338f5a6
--- /dev/null
+++ b/lib/gitlab/auth/ldap/dn.rb
@@ -0,0 +1,303 @@
+# -*- ruby encoding: utf-8 -*-
+
+# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
+#
+# For our purposes, this class is used to normalize DNs in order to allow proper
+# comparison.
+#
+# E.g. DNs should be compared case-insensitively (in basically all LDAP
+# implementations or setups), therefore we downcase every DN.
+
+##
+# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
+# ("Distinguished Name") is a unique identifier for an entry within an LDAP
+# directory. It is made up of a number of other attributes strung together,
+# to identify the entry in the tree.
+#
+# Each attribute that makes up a DN needs to have its value escaped so that
+# the DN is valid. This class helps take care of that.
+#
+# A fully escaped DN needs to be unescaped when analysing its contents. This
+# class also helps take care of that.
+module Gitlab
+ module Auth
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
+
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
+
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
+ end
+
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
+
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
+ end
+ end
+
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
+
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
+
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
+
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
+ end
+
+ str
+ end
+
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
+
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
+
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
+
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
+
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
+
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
+
+ private
+
+ def initialize_array(args)
+ buffer = StringIO.new
+
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
+ end
+
+ @dn = buffer.string
+ end
+
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
+
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
+
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/person.rb b/lib/gitlab/auth/ldap/person.rb
new file mode 100644
index 00000000000..8dfae3ee541
--- /dev/null
+++ b/lib/gitlab/auth/ldap/person.rb
@@ -0,0 +1,122 @@
+module Gitlab
+ module Auth
+ module LDAP
+ class Person
+ # Active Directory-specific LDAP filter that checks if bit 2 of the
+ # userAccountControl attribute is set.
+ # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
+ AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
+
+ InvalidEntryError = Class.new(StandardError)
+
+ attr_accessor :entry, :provider
+
+ def self.find_by_uid(uid, adapter)
+ uid = Net::LDAP::Filter.escape(uid)
+ adapter.user(adapter.config.uid, uid)
+ end
+
+ def self.find_by_dn(dn, adapter)
+ adapter.user('dn', dn)
+ end
+
+ def self.find_by_email(email, adapter)
+ email_fields = adapter.config.attributes['email']
+
+ adapter.user(email_fields, email)
+ end
+
+ def self.disabled_via_active_directory?(dn, adapter)
+ adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
+ end
+
+ def self.ldap_attributes(config)
+ [
+ 'dn',
+ config.uid,
+ *config.attributes['name'],
+ *config.attributes['email'],
+ *config.attributes['username']
+ ].compact.uniq
+ end
+
+ def self.normalize_dn(dn)
+ ::Gitlab::Auth::LDAP::DN.new(dn).to_normalized_s
+ rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
+
+ dn
+ end
+
+ # Returns the UID in a normalized form.
+ #
+ # 1. Excess spaces are stripped
+ # 2. The string is downcased (for case-insensitivity)
+ def self.normalize_uid(uid)
+ ::Gitlab::Auth::LDAP::DN.normalize_value(uid)
+ rescue ::Gitlab::Auth::LDAP::DN::FormatError => e
+ Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
+
+ uid
+ end
+
+ def initialize(entry, provider)
+ Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
+ @entry = entry
+ @provider = provider
+ end
+
+ def name
+ attribute_value(:name).first
+ end
+
+ def uid
+ entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
+ end
+
+ def username
+ username = attribute_value(:username)
+
+ # Depending on the attribute, multiple values may
+ # be returned. We need only one for username.
+ # Ex. `uid` returns only one value but `mail` may
+ # return an array of multiple email addresses.
+ [username].flatten.first.tap do |username|
+ username.downcase! if config.lowercase_usernames
+ end
+ end
+
+ def email
+ attribute_value(:email)
+ end
+
+ def dn
+ self.class.normalize_dn(entry.dn)
+ end
+
+ private
+
+ def entry
+ @entry
+ end
+
+ def config
+ @config ||= Gitlab::Auth::LDAP::Config.new(provider)
+ end
+
+ # Using the LDAP attributes configuration, find and return the first
+ # attribute with a value. For example, by default, when given 'email',
+ # this method looks for 'mail', 'email' and 'userPrincipalName' and
+ # returns the first with a value.
+ def attribute_value(attribute)
+ attributes = Array(config.attributes[attribute.to_s])
+ selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
+
+ return nil unless selected_attr
+
+ entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/ldap/user.rb b/lib/gitlab/auth/ldap/user.rb
new file mode 100644
index 00000000000..068212d9a21
--- /dev/null
+++ b/lib/gitlab/auth/ldap/user.rb
@@ -0,0 +1,54 @@
+# LDAP extension for User model
+#
+# * Find or create user from omniauth.auth data
+# * Links LDAP account with existing user
+# * Auth LDAP user with login and password
+#
+module Gitlab
+ module Auth
+ module LDAP
+ class User < Gitlab::Auth::OAuth::User
+ class << self
+ def find_by_uid_and_provider(uid, provider)
+ identity = ::Identity.with_extern_uid(provider, uid).take
+
+ identity && identity.user
+ end
+ end
+
+ def save
+ super('LDAP')
+ end
+
+ # instance methods
+ def find_user
+ find_by_uid_and_provider || find_by_email || build_new_user
+ end
+
+ def find_by_uid_and_provider
+ self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
+ end
+
+ def changed?
+ gl_user.changed? || gl_user.identities.any?(&:changed?)
+ end
+
+ def block_after_signup?
+ ldap_config.block_auto_created_users
+ end
+
+ def allowed?
+ Gitlab::Auth::LDAP::Access.allowed?(gl_user)
+ end
+
+ def ldap_config
+ Gitlab::Auth::LDAP::Config.new(auth_hash.provider)
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::Auth::LDAP::AuthHash.new(auth_hash)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/auth_hash.rb b/lib/gitlab/auth/o_auth/auth_hash.rb
new file mode 100644
index 00000000000..ed8fba94305
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/auth_hash.rb
@@ -0,0 +1,92 @@
+# Class to parse and transform the info provided by omniauth
+#
+module Gitlab
+ module Auth
+ module OAuth
+ class AuthHash
+ attr_reader :auth_hash
+ def initialize(auth_hash)
+ @auth_hash = auth_hash
+ end
+
+ def uid
+ @uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
+ end
+
+ def provider
+ @provider ||= auth_hash.provider.to_s
+ end
+
+ def name
+ @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
+ end
+
+ def username
+ @username ||= username_and_email[:username].to_s
+ end
+
+ def email
+ @email ||= username_and_email[:email].to_s
+ end
+
+ def password
+ @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
+ end
+
+ def location
+ location = get_info(:address)
+ if location.is_a?(Hash)
+ [location.locality.presence, location.country.presence].compact.join(', ')
+ else
+ location
+ end
+ end
+
+ def has_attribute?(attribute)
+ if attribute == :location
+ get_info(:address).present?
+ else
+ get_info(attribute).present?
+ end
+ end
+
+ private
+
+ def info
+ auth_hash.info
+ end
+
+ def get_info(key)
+ value = info[key]
+ Gitlab::Utils.force_utf8(value) if value
+ value
+ end
+
+ def username_and_email
+ @username_and_email ||= begin
+ username = get_info(:username).presence || get_info(:nickname).presence
+ email = get_info(:email).presence
+
+ username ||= generate_username(email) if email
+ email ||= generate_temporarily_email(username) if username
+
+ {
+ username: username,
+ email: email
+ }
+ end
+ end
+
+ # Get the first part of the email address (before @)
+ # In addtion in removes illegal characters
+ def generate_username(email)
+ email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
+ end
+
+ def generate_temporarily_email(username)
+ "temp-email-for-oauth-#{username}@gitlab.localhost"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/authentication.rb b/lib/gitlab/auth/o_auth/authentication.rb
new file mode 100644
index 00000000000..ed03b9f8b40
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/authentication.rb
@@ -0,0 +1,21 @@
+# These calls help to authenticate to OAuth provider by providing username and password
+#
+
+module Gitlab
+ module Auth
+ module OAuth
+ class Authentication
+ attr_reader :provider, :user
+
+ def initialize(provider, user = nil)
+ @provider = provider
+ @user = user
+ end
+
+ def login(login, password)
+ raise NotImplementedError
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/provider.rb b/lib/gitlab/auth/o_auth/provider.rb
new file mode 100644
index 00000000000..5fb61ffe00d
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/provider.rb
@@ -0,0 +1,73 @@
+module Gitlab
+ module Auth
+ module OAuth
+ class Provider
+ LABELS = {
+ "github" => "GitHub",
+ "gitlab" => "GitLab.com",
+ "google_oauth2" => "Google"
+ }.freeze
+
+ def self.authentication(user, provider)
+ return unless user
+ return unless enabled?(provider)
+
+ authenticator =
+ case provider
+ when /^ldap/
+ Gitlab::Auth::LDAP::Authentication
+ when 'database'
+ Gitlab::Auth::Database::Authentication
+ end
+
+ authenticator&.new(provider, user)
+ end
+
+ def self.providers
+ Devise.omniauth_providers
+ end
+
+ def self.enabled?(name)
+ return true if name == 'database'
+
+ providers.include?(name.to_sym)
+ end
+
+ def self.ldap_provider?(name)
+ name.to_s.start_with?('ldap')
+ end
+
+ def self.sync_profile_from_provider?(provider)
+ return true if ldap_provider?(provider)
+
+ providers = Gitlab.config.omniauth.sync_profile_from_provider
+
+ if providers.is_a?(Array)
+ providers.include?(provider)
+ else
+ providers
+ end
+ end
+
+ def self.config_for(name)
+ name = name.to_s
+ if ldap_provider?(name)
+ if Gitlab::Auth::LDAP::Config.valid_provider?(name)
+ Gitlab::Auth::LDAP::Config.new(name).options
+ else
+ nil
+ end
+ else
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
+ end
+ end
+
+ def self.label_for(name)
+ name = name.to_s
+ config = config_for(name)
+ (config && config['label']) || LABELS[name] || name.titleize
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/o_auth/session.rb b/lib/gitlab/auth/o_auth/session.rb
new file mode 100644
index 00000000000..8f2b4d58552
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/session.rb
@@ -0,0 +1,21 @@
+# :nocov:
+module Gitlab
+ module Auth
+ module OAuth
+ module Session
+ def self.create(provider, ticket)
+ Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
+ end
+
+ def self.destroy(provider, ticket)
+ Rails.cache.delete("gitlab:#{provider}:#{ticket}")
+ end
+
+ def self.valid?(provider, ticket)
+ Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
+ end
+ end
+ end
+ end
+end
+# :nocov:
diff --git a/lib/gitlab/auth/o_auth/user.rb b/lib/gitlab/auth/o_auth/user.rb
new file mode 100644
index 00000000000..b6a96081278
--- /dev/null
+++ b/lib/gitlab/auth/o_auth/user.rb
@@ -0,0 +1,246 @@
+# OAuth extension for User model
+#
+# * Find GitLab user based on omniauth uid and provider
+# * Create new user from omniauth data
+#
+module Gitlab
+ module Auth
+ module OAuth
+ class User
+ SignupDisabledError = Class.new(StandardError)
+ SigninDisabledForProviderError = Class.new(StandardError)
+
+ attr_accessor :auth_hash, :gl_user
+
+ def initialize(auth_hash)
+ self.auth_hash = auth_hash
+ update_profile
+ add_or_update_user_identities
+ end
+
+ def persisted?
+ gl_user.try(:persisted?)
+ end
+
+ def new?
+ !persisted?
+ end
+
+ def valid?
+ gl_user.try(:valid?)
+ end
+
+ def save(provider = 'OAuth')
+ raise SigninDisabledForProviderError if oauth_provider_disabled?
+ raise SignupDisabledError unless gl_user
+
+ block_after_save = needs_blocking?
+
+ Users::UpdateService.new(gl_user, user: gl_user).execute!
+
+ gl_user.block if block_after_save
+
+ log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
+ gl_user
+ rescue ActiveRecord::RecordInvalid => e
+ log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
+ return self, e.record.errors
+ end
+
+ def gl_user
+ return @gl_user if defined?(@gl_user)
+
+ @gl_user = find_user
+ end
+
+ def find_user
+ user = find_by_uid_and_provider
+
+ user ||= find_or_build_ldap_user if auto_link_ldap_user?
+ user ||= build_new_user if signup_enabled?
+
+ user.external = true if external_provider? && user&.new_record?
+
+ user
+ end
+
+ protected
+
+ def add_or_update_user_identities
+ return unless gl_user
+
+ # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
+ identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
+
+ identity ||= gl_user.identities.build(provider: auth_hash.provider)
+ identity.extern_uid = auth_hash.uid
+
+ if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person
+ log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}."
+ gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn)
+ end
+ end
+
+ def find_or_build_ldap_user
+ return unless ldap_person
+
+ user = Gitlab::Auth::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
+ if user
+ log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
+ return user
+ end
+
+ log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
+ build_new_user
+ end
+
+ def find_by_email
+ return unless auth_hash.has_attribute?(:email)
+
+ ::User.find_by(email: auth_hash.email.downcase)
+ end
+
+ def auto_link_ldap_user?
+ Gitlab.config.omniauth.auto_link_ldap_user
+ end
+
+ def creating_linked_ldap_user?
+ auto_link_ldap_user? && ldap_person
+ end
+
+ def ldap_person
+ return @ldap_person if defined?(@ldap_person)
+
+ # Look for a corresponding person with same uid in any of the configured LDAP providers
+ Gitlab::Auth::LDAP::Config.providers.each do |provider|
+ adapter = Gitlab::Auth::LDAP::Adapter.new(provider)
+ @ldap_person = find_ldap_person(auth_hash, adapter)
+ break if @ldap_person
+ end
+ @ldap_person
+ end
+
+ def find_ldap_person(auth_hash, adapter)
+ Gitlab::Auth::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
+ Gitlab::Auth::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
+ Gitlab::Auth::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
+ end
+
+ def ldap_config
+ Gitlab::Auth::LDAP::Config.new(ldap_person.provider) if ldap_person
+ end
+
+ def needs_blocking?
+ new? && block_after_signup?
+ end
+
+ def signup_enabled?
+ providers = Gitlab.config.omniauth.allow_single_sign_on
+ if providers.is_a?(Array)
+ providers.include?(auth_hash.provider)
+ else
+ providers
+ end
+ end
+
+ def external_provider?
+ Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
+ end
+
+ def block_after_signup?
+ if creating_linked_ldap_user?
+ ldap_config.block_auto_created_users
+ else
+ Gitlab.config.omniauth.block_auto_created_users
+ end
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = AuthHash.new(auth_hash)
+ end
+
+ def find_by_uid_and_provider
+ identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
+ identity&.user
+ end
+
+ def build_new_user
+ user_params = user_attributes.merge(skip_confirmation: true)
+ Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
+ end
+
+ def user_attributes
+ # Give preference to LDAP for sensitive information when creating a linked account
+ if creating_linked_ldap_user?
+ username = ldap_person.username.presence
+ email = ldap_person.email.first.presence
+ end
+
+ username ||= auth_hash.username
+ email ||= auth_hash.email
+
+ valid_username = ::Namespace.clean_path(username)
+
+ uniquify = Uniquify.new
+ valid_username = uniquify.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
+
+ name = auth_hash.name
+ name = valid_username if name.strip.empty?
+
+ {
+ name: name,
+ username: valid_username,
+ email: email,
+ password: auth_hash.password,
+ password_confirmation: auth_hash.password,
+ password_automatically_set: true
+ }
+ end
+
+ def sync_profile_from_provider?
+ Gitlab::Auth::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
+ end
+
+ def update_profile
+ clear_user_synced_attributes_metadata
+
+ return unless sync_profile_from_provider? || creating_linked_ldap_user?
+
+ metadata = gl_user.build_user_synced_attributes_metadata
+
+ if sync_profile_from_provider?
+ UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
+ if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
+ gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
+ metadata.set_attribute_synced(key, true)
+ else
+ metadata.set_attribute_synced(key, false)
+ end
+ end
+
+ metadata.provider = auth_hash.provider
+ end
+
+ if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first
+ metadata.set_attribute_synced(:email, true)
+ metadata.provider = ldap_person.provider
+ end
+ end
+
+ def clear_user_synced_attributes_metadata
+ gl_user&.user_synced_attributes_metadata&.destroy
+ end
+
+ def log
+ Gitlab::AppLogger
+ end
+
+ def oauth_provider_disabled?
+ Gitlab::CurrentSettings.current_application_settings
+ .disabled_oauth_sign_in_sources
+ .include?(auth_hash.provider)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/auth_hash.rb b/lib/gitlab/auth/saml/auth_hash.rb
new file mode 100644
index 00000000000..c345a7e3f6c
--- /dev/null
+++ b/lib/gitlab/auth/saml/auth_hash.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Auth
+ module Saml
+ class AuthHash < Gitlab::Auth::OAuth::AuthHash
+ def groups
+ Array.wrap(get_raw(Gitlab::Auth::Saml::Config.groups))
+ end
+
+ private
+
+ def get_raw(key)
+ # Needs to call `all` because of https://git.io/vVo4u
+ # otherwise just the first value is returned
+ auth_hash.extra[:raw_info].all[key]
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/config.rb b/lib/gitlab/auth/saml/config.rb
new file mode 100644
index 00000000000..e654e7fe438
--- /dev/null
+++ b/lib/gitlab/auth/saml/config.rb
@@ -0,0 +1,21 @@
+module Gitlab
+ module Auth
+ module Saml
+ class Config
+ class << self
+ def options
+ Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
+ end
+
+ def groups
+ options[:groups_attribute]
+ end
+
+ def external_groups
+ options[:external_groups]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/auth/saml/user.rb b/lib/gitlab/auth/saml/user.rb
new file mode 100644
index 00000000000..d4024e9ec39
--- /dev/null
+++ b/lib/gitlab/auth/saml/user.rb
@@ -0,0 +1,52 @@
+# SAML extension for User model
+#
+# * Find GitLab user based on SAML uid and provider
+# * Create new user from SAML data
+#
+module Gitlab
+ module Auth
+ module Saml
+ class User < Gitlab::Auth::OAuth::User
+ def save
+ super('SAML')
+ end
+
+ def find_user
+ user = find_by_uid_and_provider
+
+ user ||= find_by_email if auto_link_saml_user?
+ user ||= find_or_build_ldap_user if auto_link_ldap_user?
+ user ||= build_new_user if signup_enabled?
+
+ if external_users_enabled? && user
+ # Check if there is overlap between the user's groups and the external groups
+ # setting then set user as external or internal.
+ user.external = !(auth_hash.groups & Gitlab::Auth::Saml::Config.external_groups).empty?
+ end
+
+ user
+ end
+
+ def changed?
+ return true unless gl_user
+
+ gl_user.changed? || gl_user.identities.any?(&:changed?)
+ end
+
+ protected
+
+ def auto_link_saml_user?
+ Gitlab.config.omniauth.auto_link_saml_user
+ end
+
+ def external_users_enabled?
+ !Gitlab::Auth::Saml::Config.external_groups.nil?
+ end
+
+ def auth_hash=(auth_hash)
+ @auth_hash = Gitlab::Auth::Saml::AuthHash.new(auth_hash)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/migrate_build_stage.rb b/lib/gitlab/background_migration/migrate_build_stage.rb
new file mode 100644
index 00000000000..8fe4f1a2289
--- /dev/null
+++ b/lib/gitlab/background_migration/migrate_build_stage.rb
@@ -0,0 +1,48 @@
+# frozen_string_literal: true
+# rubocop:disable Metrics/AbcSize
+# rubocop:disable Style/Documentation
+
+module Gitlab
+ module BackgroundMigration
+ class MigrateBuildStage
+ module Migratable
+ class Stage < ActiveRecord::Base
+ self.table_name = 'ci_stages'
+ end
+
+ class Build < ActiveRecord::Base
+ self.table_name = 'ci_builds'
+
+ def ensure_stage!(attempts: 2)
+ find_stage || create_stage!
+ rescue ActiveRecord::RecordNotUnique
+ retry if (attempts -= 1) > 0
+ raise
+ end
+
+ def find_stage
+ Stage.find_by(name: self.stage || 'test',
+ pipeline_id: self.commit_id,
+ project_id: self.project_id)
+ end
+
+ def create_stage!
+ Stage.create!(name: self.stage || 'test',
+ pipeline_id: self.commit_id,
+ project_id: self.project_id)
+ end
+ end
+ end
+
+ def perform(start_id, stop_id)
+ stages = Migratable::Build.where('stage_id IS NULL')
+ .where('id BETWEEN ? AND ?', start_id, stop_id)
+ .map { |build| build.ensure_stage! }
+ .compact.map(&:id)
+
+ MigrateBuildStageIdReference.new.perform(start_id, stop_id)
+ MigrateStageStatus.new.perform(stages.min, stages.max)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
index 85749366bfd..d9d3d2e667b 100644
--- a/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
+++ b/lib/gitlab/background_migration/normalize_ldap_extern_uids_range.rb
@@ -16,281 +16,283 @@ module Gitlab
# And if the normalize behavior is changed in the future, it must be
# accompanied by another migration.
module Gitlab
- module LDAP
- class DN
- FormatError = Class.new(StandardError)
- MalformedError = Class.new(FormatError)
- UnsupportedError = Class.new(FormatError)
+ module Auth
+ module LDAP
+ class DN
+ FormatError = Class.new(StandardError)
+ MalformedError = Class.new(FormatError)
+ UnsupportedError = Class.new(FormatError)
- def self.normalize_value(given_value)
- dummy_dn = "placeholder=#{given_value}"
- normalized_dn = new(*dummy_dn).to_normalized_s
- normalized_dn.sub(/\Aplaceholder=/, '')
- end
+ def self.normalize_value(given_value)
+ dummy_dn = "placeholder=#{given_value}"
+ normalized_dn = new(*dummy_dn).to_normalized_s
+ normalized_dn.sub(/\Aplaceholder=/, '')
+ end
- ##
- # Initialize a DN, escaping as required. Pass in attributes in name/value
- # pairs. If there is a left over argument, it will be appended to the dn
- # without escaping (useful for a base string).
- #
- # Most uses of this class will be to escape a DN, rather than to parse it,
- # so storing the dn as an escaped String and parsing parts as required
- # with a state machine seems sensible.
- def initialize(*args)
- if args.length > 1
- initialize_array(args)
- else
- initialize_string(args[0])
+ ##
+ # Initialize a DN, escaping as required. Pass in attributes in name/value
+ # pairs. If there is a left over argument, it will be appended to the dn
+ # without escaping (useful for a base string).
+ #
+ # Most uses of this class will be to escape a DN, rather than to parse it,
+ # so storing the dn as an escaped String and parsing parts as required
+ # with a state machine seems sensible.
+ def initialize(*args)
+ if args.length > 1
+ initialize_array(args)
+ else
+ initialize_string(args[0])
+ end
end
- end
- ##
- # Parse a DN into key value pairs using ASN from
- # http://tools.ietf.org/html/rfc2253 section 3.
- # rubocop:disable Metrics/AbcSize
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def each_pair
- state = :key
- key = StringIO.new
- value = StringIO.new
- hex_buffer = ""
+ ##
+ # Parse a DN into key value pairs using ASN from
+ # http://tools.ietf.org/html/rfc2253 section 3.
+ # rubocop:disable Metrics/AbcSize
+ # rubocop:disable Metrics/CyclomaticComplexity
+ # rubocop:disable Metrics/PerceivedComplexity
+ def each_pair
+ state = :key
+ key = StringIO.new
+ value = StringIO.new
+ hex_buffer = ""
- @dn.each_char.with_index do |char, dn_index|
- case state
- when :key then
- case char
- when 'a'..'z', 'A'..'Z' then
- state = :key_normal
- key << char
- when '0'..'9' then
- state = :key_oid
- key << char
- when ' ' then state = :key
- else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
- end
- when :key_normal then
- case char
- when '=' then state = :value
- when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
- end
- when :key_oid then
- case char
- when '=' then state = :value
- when '0'..'9', '.', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
- end
- when :value then
- case char
- when '\\' then state = :value_normal_escape
- when '"' then state = :value_quoted
- when ' ' then state = :value
- when '#' then
- state = :value_hexstring
- value << char
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else
- state = :value_normal
- value << char
- end
- when :value_normal then
- case char
- when '\\' then state = :value_normal_escape
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
- else value << char
- end
- when :value_normal_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal_escape_hex
- hex_buffer = char
- else
- state = :value_normal
- value << char
+ @dn.each_char.with_index do |char, dn_index|
+ case state
+ when :key then
+ case char
+ when 'a'..'z', 'A'..'Z' then
+ state = :key_normal
+ key << char
+ when '0'..'9' then
+ state = :key_oid
+ key << char
+ when ' ' then state = :key
+ else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
+ end
+ when :key_normal then
+ case char
+ when '=' then state = :value
+ when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
+ end
+ when :key_oid then
+ case char
+ when '=' then state = :value
+ when '0'..'9', '.', ' ' then key << char
+ else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
+ end
+ when :value then
+ case char
+ when '\\' then state = :value_normal_escape
+ when '"' then state = :value_quoted
+ when ' ' then state = :value
+ when '#' then
+ state = :value_hexstring
+ value << char
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal then
+ case char
+ when '\\' then state = :value_normal_escape
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
+ else value << char
+ end
+ when :value_normal_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal_escape_hex
+ hex_buffer = char
+ else
+ state = :value_normal
+ value << char
+ end
+ when :value_normal_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_normal
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
+ end
+ when :value_quoted then
+ case char
+ when '\\' then state = :value_quoted_escape
+ when '"' then state = :value_end
+ else value << char
+ end
+ when :value_quoted_escape then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted_escape_hex
+ hex_buffer = char
+ else
+ state = :value_quoted
+ value << char
+ end
+ when :value_quoted_escape_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_quoted
+ value << "#{hex_buffer}#{char}".to_i(16).chr
+ else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
+ end
+ when :value_hexstring then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring_hex
+ value << char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_hexstring_hex then
+ case char
+ when '0'..'9', 'a'..'f', 'A'..'F' then
+ state = :value_hexstring
+ value << char
+ else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
+ end
+ when :value_end then
+ case char
+ when ' ' then state = :value_end
+ when ',' then
+ state = :key
+ yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
+ key = StringIO.new
+ value = StringIO.new
+ else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
+ end
+ else raise "Fell out of state machine"
end
- when :value_normal_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
- end
- when :value_quoted then
- case char
- when '\\' then state = :value_quoted_escape
- when '"' then state = :value_end
- else value << char
- end
- when :value_quoted_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted_escape_hex
- hex_buffer = char
- else
- state = :value_quoted
- value << char
- end
- when :value_quoted_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
- end
- when :value_hexstring then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring_hex
- value << char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
- end
- when :value_hexstring_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring
- value << char
- else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
- end
- when :value_end then
- case char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
- end
- else raise "Fell out of state machine"
end
- end
- # Last pair
- raise(MalformedError, 'DN string ended unexpectedly') unless
- [:value, :value_normal, :value_hexstring, :value_end].include? state
+ # Last pair
+ raise(MalformedError, 'DN string ended unexpectedly') unless
+ [:value, :value_normal, :value_hexstring, :value_end].include? state
- yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
- end
+ yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
+ end
- def rstrip_except_escaped(str, dn_index)
- str_ends_with_whitespace = str.match(/\s\z/)
+ def rstrip_except_escaped(str, dn_index)
+ str_ends_with_whitespace = str.match(/\s\z/)
- if str_ends_with_whitespace
- dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
+ if str_ends_with_whitespace
+ dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
- if dn_part_ends_with_escaped_whitespace
- dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
- num_chars_to_remove = dn_part_rwhitespace.length - 1
- str = str[0, str.length - num_chars_to_remove]
- else
- str.rstrip!
+ if dn_part_ends_with_escaped_whitespace
+ dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
+ num_chars_to_remove = dn_part_rwhitespace.length - 1
+ str = str[0, str.length - num_chars_to_remove]
+ else
+ str.rstrip!
+ end
end
- end
- str
- end
+ str
+ end
- ##
- # Returns the DN as an array in the form expected by the constructor.
- def to_a
- a = []
- self.each_pair { |key, value| a << key << value } unless @dn.empty?
- a
- end
+ ##
+ # Returns the DN as an array in the form expected by the constructor.
+ def to_a
+ a = []
+ self.each_pair { |key, value| a << key << value } unless @dn.empty?
+ a
+ end
- ##
- # Return the DN as an escaped string.
- def to_s
- @dn
- end
+ ##
+ # Return the DN as an escaped string.
+ def to_s
+ @dn
+ end
- ##
- # Return the DN as an escaped and normalized string.
- def to_normalized_s
- self.class.new(*to_a).to_s.downcase
- end
+ ##
+ # Return the DN as an escaped and normalized string.
+ def to_normalized_s
+ self.class.new(*to_a).to_s.downcase
+ end
- # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
- # for DN values. All of the following must be escaped in any normal string
- # using a single backslash ('\') as escape. The space character is left
- # out here because in a "normalized" string, spaces should only be escaped
- # if necessary (i.e. leading or trailing space).
- NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
+ # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
+ # for DN values. All of the following must be escaped in any normal string
+ # using a single backslash ('\') as escape. The space character is left
+ # out here because in a "normalized" string, spaces should only be escaped
+ # if necessary (i.e. leading or trailing space).
+ NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
- # The following must be represented as escaped hex
- HEX_ESCAPES = {
- "\n" => '\0a',
- "\r" => '\0d'
- }.freeze
+ # The following must be represented as escaped hex
+ HEX_ESCAPES = {
+ "\n" => '\0a',
+ "\r" => '\0d'
+ }.freeze
- # Compiled character class regexp using the keys from the above hash, and
- # checking for a space or # at the start, or space at the end, of the
- # string.
- ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
- NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
- "])")
+ # Compiled character class regexp using the keys from the above hash, and
+ # checking for a space or # at the start, or space at the end, of the
+ # string.
+ ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
+ NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
+ "])")
- HEX_ESCAPE_RE = Regexp.new("([" +
- HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
- "])")
+ HEX_ESCAPE_RE = Regexp.new("([" +
+ HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
+ "])")
- ##
- # Escape a string for use in a DN value
- def self.escape(string)
- escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
- escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
- end
+ ##
+ # Escape a string for use in a DN value
+ def self.escape(string)
+ escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
+ escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
+ end
- private
+ private
- def initialize_array(args)
- buffer = StringIO.new
+ def initialize_array(args)
+ buffer = StringIO.new
- args.each_with_index do |arg, index|
- if index.even? # key
- buffer << "," if index > 0
- buffer << arg
- else # value
- buffer << "="
- buffer << self.class.escape(arg)
+ args.each_with_index do |arg, index|
+ if index.even? # key
+ buffer << "," if index > 0
+ buffer << arg
+ else # value
+ buffer << "="
+ buffer << self.class.escape(arg)
+ end
end
- end
- @dn = buffer.string
- end
+ @dn = buffer.string
+ end
- def initialize_string(arg)
- @dn = arg.to_s
- end
+ def initialize_string(arg)
+ @dn = arg.to_s
+ end
- ##
- # Proxy all other requests to the string object, because a DN is mainly
- # used within the library as a string
- # rubocop:disable GitlabSecurity/PublicSend
- def method_missing(method, *args, &block)
- @dn.send(method, *args, &block)
- end
+ ##
+ # Proxy all other requests to the string object, because a DN is mainly
+ # used within the library as a string
+ # rubocop:disable GitlabSecurity/PublicSend
+ def method_missing(method, *args, &block)
+ @dn.send(method, *args, &block)
+ end
- ##
- # Redefined to be consistent with redefined `method_missing` behavior
- def respond_to?(sym, include_private = false)
- @dn.respond_to?(sym, include_private)
+ ##
+ # Redefined to be consistent with redefined `method_missing` behavior
+ def respond_to?(sym, include_private = false)
+ @dn.respond_to?(sym, include_private)
+ end
end
end
end
@@ -302,11 +304,11 @@ module Gitlab
ldap_identities = Identity.where("provider like 'ldap%'").where(id: start_id..end_id)
ldap_identities.each do |identity|
begin
- identity.extern_uid = Gitlab::LDAP::DN.new(identity.extern_uid).to_normalized_s
+ identity.extern_uid = Gitlab::Auth::LDAP::DN.new(identity.extern_uid).to_normalized_s
unless identity.save
Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\". Skipping."
end
- rescue Gitlab::LDAP::DN::FormatError => e
+ rescue Gitlab::Auth::LDAP::DN::FormatError => e
Rails.logger.info "Unable to normalize \"#{identity.extern_uid}\" due to \"#{e.message}\". Skipping."
end
end
diff --git a/lib/gitlab/ci/pipeline/chain/create.rb b/lib/gitlab/ci/pipeline/chain/create.rb
index d19a2519803..d5e17a123df 100644
--- a/lib/gitlab/ci/pipeline/chain/create.rb
+++ b/lib/gitlab/ci/pipeline/chain/create.rb
@@ -17,27 +17,11 @@ module Gitlab
end
rescue ActiveRecord::RecordInvalid => e
error("Failed to persist the pipeline: #{e}")
- ensure
- if pipeline.builds.where(stage_id: nil).any?
- invalid_builds_counter.increment(node: hostname)
- end
end
def break?
!pipeline.persisted?
end
-
- private
-
- def invalid_builds_counter
- @counter ||= Gitlab::Metrics
- .counter(:gitlab_ci_invalid_builds_total,
- 'Invalid builds without stage assigned counter')
- end
-
- def hostname
- @hostname ||= Socket.gethostname
- end
end
end
end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/base.rb b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
new file mode 100644
index 00000000000..047ab66e9b3
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/base.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Base
+ def evaluate(**variables)
+ raise NotImplementedError
+ end
+
+ def self.build(token)
+ raise NotImplementedError
+ end
+
+ def self.scan(scanner)
+ if scanner.scan(self::PATTERN)
+ Expression::Token.new(scanner.matched, self)
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
new file mode 100644
index 00000000000..3a2f0c6924e
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/equals.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Equals < Lexeme::Operator
+ PATTERN = /==/.freeze
+
+ def initialize(left, right)
+ @left = left
+ @right = right
+ end
+
+ def evaluate(variables = {})
+ @left.evaluate(variables) == @right.evaluate(variables)
+ end
+
+ def self.build(_value, behind, ahead)
+ new(behind, ahead)
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/null.rb b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
new file mode 100644
index 00000000000..a2778716924
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/null.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Null < Lexeme::Value
+ PATTERN = /null/.freeze
+
+ def initialize(value = nil)
+ @value = nil
+ end
+
+ def evaluate(variables = {})
+ nil
+ end
+
+ def self.build(_value)
+ self.new
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
new file mode 100644
index 00000000000..f640d0b5855
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/operator.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Operator < Lexeme::Base
+ def self.type
+ :operator
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/string.rb b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
new file mode 100644
index 00000000000..48bde213d44
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/string.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class String < Lexeme::Value
+ PATTERN = /("(?<string>.+?)")|('(?<string>.+?)')/.freeze
+
+ def initialize(value)
+ @value = value
+ end
+
+ def evaluate(variables = {})
+ @value.to_s
+ end
+
+ def self.build(string)
+ new(string.match(PATTERN)[:string])
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/value.rb b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
new file mode 100644
index 00000000000..f2611d65faf
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/value.rb
@@ -0,0 +1,15 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Value < Lexeme::Base
+ def self.type
+ :value
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
new file mode 100644
index 00000000000..b781c15fd67
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexeme/variable.rb
@@ -0,0 +1,25 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ module Lexeme
+ class Variable < Lexeme::Value
+ PATTERN = /\$(?<name>\w+)/.freeze
+
+ def initialize(name)
+ @name = name
+ end
+
+ def evaluate(variables = {})
+ HashWithIndifferentAccess.new(variables).fetch(@name, nil)
+ end
+
+ def self.build(string)
+ new(string.match(PATTERN)[:name])
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/lexer.rb b/lib/gitlab/ci/pipeline/expression/lexer.rb
new file mode 100644
index 00000000000..e1c68b7c3c2
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/lexer.rb
@@ -0,0 +1,59 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Lexer
+ include ::Gitlab::Utils::StrongMemoize
+
+ LEXEMES = [
+ Expression::Lexeme::Variable,
+ Expression::Lexeme::String,
+ Expression::Lexeme::Null,
+ Expression::Lexeme::Equals
+ ].freeze
+
+ SyntaxError = Class.new(Statement::StatementError)
+
+ MAX_TOKENS = 100
+
+ def initialize(statement, max_tokens: MAX_TOKENS)
+ @scanner = StringScanner.new(statement)
+ @max_tokens = max_tokens
+ end
+
+ def tokens
+ strong_memoize(:tokens) { tokenize }
+ end
+
+ def lexemes
+ tokens.map(&:to_lexeme)
+ end
+
+ private
+
+ def tokenize
+ tokens = []
+
+ @max_tokens.times do
+ @scanner.skip(/\s+/) # ignore whitespace
+
+ return tokens if @scanner.eos?
+
+ lexeme = LEXEMES.find do |type|
+ type.scan(@scanner).tap do |token|
+ tokens.push(token) if token.present?
+ end
+ end
+
+ unless lexeme.present?
+ raise Lexer::SyntaxError, 'Unknown lexeme found!'
+ end
+ end
+
+ raise Lexer::SyntaxError, 'Too many tokens!'
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/parser.rb b/lib/gitlab/ci/pipeline/expression/parser.rb
new file mode 100644
index 00000000000..90f94d0b763
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/parser.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Parser
+ def initialize(tokens)
+ @tokens = tokens.to_enum
+ @nodes = []
+ end
+
+ ##
+ # This produces a reverse descent parse tree.
+ #
+ # It currently does not support precedence of operators.
+ #
+ def tree
+ while token = @tokens.next
+ case token.type
+ when :operator
+ token.build(@nodes.pop, tree).tap do |node|
+ @nodes.push(node)
+ end
+ when :value
+ token.build.tap do |leaf|
+ @nodes.push(leaf)
+ end
+ end
+ end
+ rescue StopIteration
+ @nodes.last || Lexeme::Null.new
+ end
+
+ def self.seed(statement)
+ new(Expression::Lexer.new(statement).tokens)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/statement.rb b/lib/gitlab/ci/pipeline/expression/statement.rb
new file mode 100644
index 00000000000..4f0e101b730
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/statement.rb
@@ -0,0 +1,42 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Statement
+ StatementError = Class.new(StandardError)
+
+ GRAMMAR = [
+ %w[variable equals string],
+ %w[variable equals variable],
+ %w[variable equals null],
+ %w[string equals variable],
+ %w[null equals variable],
+ %w[variable]
+ ].freeze
+
+ def initialize(statement, pipeline)
+ @lexer = Expression::Lexer.new(statement)
+
+ @variables = pipeline.variables.map do |variable|
+ [variable.key, variable.value]
+ end
+ end
+
+ def parse_tree
+ raise StatementError if @lexer.lexemes.empty?
+
+ unless GRAMMAR.find { |syntax| syntax == @lexer.lexemes }
+ raise StatementError, 'Unknown pipeline expression!'
+ end
+
+ Expression::Parser.new(@lexer.tokens).tree
+ end
+
+ def evaluate
+ parse_tree.evaluate(@variables.to_h)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/ci/pipeline/expression/token.rb b/lib/gitlab/ci/pipeline/expression/token.rb
new file mode 100644
index 00000000000..58211800b88
--- /dev/null
+++ b/lib/gitlab/ci/pipeline/expression/token.rb
@@ -0,0 +1,28 @@
+module Gitlab
+ module Ci
+ module Pipeline
+ module Expression
+ class Token
+ attr_reader :value, :lexeme
+
+ def initialize(value, lexeme)
+ @value = value
+ @lexeme = lexeme
+ end
+
+ def build(*args)
+ @lexeme.build(@value, *args)
+ end
+
+ def type
+ @lexeme.type
+ end
+
+ def to_lexeme
+ @lexeme.name.demodulize.downcase
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/contributions_calendar.rb b/lib/gitlab/contributions_calendar.rb
index 9576d5a3fd8..02d3763514e 100644
--- a/lib/gitlab/contributions_calendar.rb
+++ b/lib/gitlab/contributions_calendar.rb
@@ -23,7 +23,7 @@ module Gitlab
mr_events = event_counts(date_from, :merge_requests)
.having(action: [Event::MERGED, Event::CREATED, Event::CLOSED], target_type: "MergeRequest")
note_events = event_counts(date_from, :merge_requests)
- .having(action: [Event::COMMENTED], target_type: "Note")
+ .having(action: [Event::COMMENTED], target_type: %w(Note DiffNote))
union = Gitlab::SQL::Union.new([repo_events, issue_events, mr_events, note_events])
events = Event.find_by_sql(union.to_sql).map(&:attributes)
diff --git a/lib/gitlab/cycle_analytics/base_query.rb b/lib/gitlab/cycle_analytics/base_query.rb
index 8b3bc3e440d..86d708be0d6 100644
--- a/lib/gitlab/cycle_analytics/base_query.rb
+++ b/lib/gitlab/cycle_analytics/base_query.rb
@@ -8,13 +8,14 @@ module Gitlab
private
def base_query
- @base_query ||= stage_query
+ @base_query ||= stage_query(@project.id) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
- def stage_query
+ def stage_query(project_ids)
query = mr_closing_issues_table.join(issue_table).on(issue_table[:id].eq(mr_closing_issues_table[:issue_id]))
.join(issue_metrics_table).on(issue_table[:id].eq(issue_metrics_table[:issue_id]))
- .where(issue_table[:project_id].eq(@project.id)) # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ .project(issue_table[:project_id].as("project_id"))
+ .where(issue_table[:project_id].in(project_ids))
.where(issue_table[:created_at].gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
# Load merge_requests
diff --git a/lib/gitlab/cycle_analytics/base_stage.rb b/lib/gitlab/cycle_analytics/base_stage.rb
index cac31ea8cff..038d5a19bc4 100644
--- a/lib/gitlab/cycle_analytics/base_stage.rb
+++ b/lib/gitlab/cycle_analytics/base_stage.rb
@@ -21,17 +21,28 @@ module Gitlab
end
def median
- cte_table = Arel::Table.new("cte_table_for_#{name}")
+ BatchLoader.for(@project.id).batch(key: name) do |project_ids, loader|
+ cte_table = Arel::Table.new("cte_table_for_#{name}")
- # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
- # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
- # We compute the (end_time - start_time) interval, and give it an alias based on the current
- # cycle analytics stage.
- interval_query = Arel::Nodes::As.new(
- cte_table,
- subtract_datetimes(base_query.dup, start_time_attrs, end_time_attrs, name.to_s))
+ # Build a `SELECT` query. We find the first of the `end_time_attrs` that isn't `NULL` (call this end_time).
+ # Next, we find the first of the start_time_attrs that isn't `NULL` (call this start_time).
+ # We compute the (end_time - start_time) interval, and give it an alias based on the current
+ # cycle analytics stage.
+ interval_query = Arel::Nodes::As.new(cte_table,
+ subtract_datetimes(stage_query(project_ids), start_time_attrs, end_time_attrs, name.to_s))
- median_datetime(cte_table, interval_query, name)
+ if project_ids.one?
+ loader.call(@project.id, median_datetime(cte_table, interval_query, name))
+ else
+ begin
+ median_datetimes(cte_table, interval_query, name, :project_id)&.each do |project_id, median|
+ loader.call(project_id, median)
+ end
+ rescue NotSupportedError
+ {}
+ end
+ end
+ end
end
def name
diff --git a/lib/gitlab/cycle_analytics/production_helper.rb b/lib/gitlab/cycle_analytics/production_helper.rb
index 7a889b3877f..d0ca62e46e4 100644
--- a/lib/gitlab/cycle_analytics/production_helper.rb
+++ b/lib/gitlab/cycle_analytics/production_helper.rb
@@ -1,8 +1,8 @@
module Gitlab
module CycleAnalytics
module ProductionHelper
- def stage_query
- super
+ def stage_query(project_ids)
+ super(project_ids)
.where(mr_metrics_table[:first_deployed_to_production_at]
.gteq(@options[:from])) # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
diff --git a/lib/gitlab/cycle_analytics/test_stage.rb b/lib/gitlab/cycle_analytics/test_stage.rb
index 2b5f72bef89..0e9d235ca79 100644
--- a/lib/gitlab/cycle_analytics/test_stage.rb
+++ b/lib/gitlab/cycle_analytics/test_stage.rb
@@ -25,11 +25,11 @@ module Gitlab
_("Total test time for all commits/merges")
end
- def stage_query
+ def stage_query(project_ids)
if @options[:branch]
- super.where(build_table[:ref].eq(@options[:branch]))
+ super(project_ids).where(build_table[:ref].eq(@options[:branch]))
else
- super
+ super(project_ids)
end
end
end
diff --git a/lib/gitlab/cycle_analytics/usage_data.rb b/lib/gitlab/cycle_analytics/usage_data.rb
new file mode 100644
index 00000000000..5122e3417ca
--- /dev/null
+++ b/lib/gitlab/cycle_analytics/usage_data.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module CycleAnalytics
+ class UsageData
+ PROJECTS_LIMIT = 10
+
+ attr_reader :projects, :options
+
+ def initialize
+ @projects = Project.sorted_by_activity.limit(PROJECTS_LIMIT)
+ @options = { from: 7.days.ago }
+ end
+
+ def to_json
+ total = 0
+
+ values =
+ medians_per_stage.each_with_object({}) do |(stage_name, medians), hsh|
+ calculations = stage_values(medians)
+
+ total += calculations.values.compact.sum
+ hsh[stage_name] = calculations
+ end
+
+ values[:total] = total
+
+ { avg_cycle_analytics: values }
+ end
+
+ private
+
+ def medians_per_stage
+ projects.each_with_object({}) do |project, hsh|
+ ::CycleAnalytics.new(project, options).all_medians_per_stage.each do |stage_name, median|
+ hsh[stage_name] ||= []
+ hsh[stage_name] << median
+ end
+ end
+ end
+
+ def stage_values(medians)
+ medians = medians.map(&:presence).compact
+ average = calc_average(medians)
+
+ {
+ average: average,
+ sd: standard_deviation(medians, average),
+ missing: projects.length - medians.length
+ }
+ end
+
+ def calc_average(values)
+ return if values.empty?
+
+ (values.sum / values.length).to_i
+ end
+
+ def standard_deviation(values, average)
+ Math.sqrt(sample_variance(values, average)).to_i
+ end
+
+ def sample_variance(values, average)
+ return 0 if values.length <= 1
+
+ sum = values.inject(0) do |acc, val|
+ acc + (val - average)**2
+ end
+
+ sum / (values.length - 1)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/median.rb b/lib/gitlab/database/median.rb
index 059054ac9ff..74fed447289 100644
--- a/lib/gitlab/database/median.rb
+++ b/lib/gitlab/database/median.rb
@@ -2,18 +2,14 @@
module Gitlab
module Database
module Median
+ NotSupportedError = Class.new(StandardError)
+
def median_datetime(arel_table, query_so_far, column_sym)
- median_queries =
- if Gitlab::Database.postgresql?
- pg_median_datetime_sql(arel_table, query_so_far, column_sym)
- elsif Gitlab::Database.mysql?
- mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
- end
-
- results = Array.wrap(median_queries).map do |query|
- ActiveRecord::Base.connection.execute(query)
- end
- extract_median(results).presence
+ extract_median(execute_queries(arel_table, query_so_far, column_sym)).presence
+ end
+
+ def median_datetimes(arel_table, query_so_far, column_sym, partition_column)
+ extract_medians(execute_queries(arel_table, query_so_far, column_sym, partition_column)).presence
end
def extract_median(results)
@@ -21,13 +17,21 @@ module Gitlab
if Gitlab::Database.postgresql?
result = result.first.presence
- median = result['median'] if result
- median.to_f if median
+
+ result['median']&.to_f if result
elsif Gitlab::Database.mysql?
result.to_a.flatten.first
end
end
+ def extract_medians(results)
+ median_values = results.compact.first.values
+
+ median_values.each_with_object({}) do |(id, median), hash|
+ hash[id.to_i] = median&.to_f
+ end
+ end
+
def mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
query = arel_table
.from(arel_table.project(Arel.sql('*')).order(arel_table[column_sym]).as(arel_table.table_name))
@@ -53,7 +57,7 @@ module Gitlab
]
end
- def pg_median_datetime_sql(arel_table, query_so_far, column_sym)
+ def pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column = nil)
# Create a CTE with the column we're operating on, row number (after sorting by the column
# we're operating on), and count of the table we're operating on (duplicated across) all rows
# of the CTE. For example, if we're looking to find the median of the `projects.star_count`
@@ -64,41 +68,107 @@ module Gitlab
# 5 | 1 | 3
# 9 | 2 | 3
# 15 | 3 | 3
+ #
+ # If a partition column is used we will do the same operation but for separate partitions,
+ # when that happens the CTE might look like this:
+ #
+ # project_id | star_count | row_id | ct
+ # ------------+------------+--------+----
+ # 1 | 5 | 1 | 2
+ # 1 | 9 | 2 | 2
+ # 2 | 10 | 1 | 3
+ # 2 | 15 | 2 | 3
+ # 2 | 20 | 3 | 3
cte_table = Arel::Table.new("ordered_records")
+
cte = Arel::Nodes::As.new(
cte_table,
- arel_table
- .project(
- arel_table[column_sym].as(column_sym.to_s),
- Arel::Nodes::Over.new(Arel::Nodes::NamedFunction.new("row_number", []),
- Arel::Nodes::Window.new.order(arel_table[column_sym])).as('row_id'),
- arel_table.project("COUNT(1)").as('ct')).
+ arel_table.project(*rank_rows(arel_table, column_sym, partition_column)).
# Disallow negative values
where(arel_table[column_sym].gteq(zero_interval)))
# From the CTE, select either the middle row or the middle two rows (this is accomplished
# by 'where cte.row_id between cte.ct / 2.0 AND cte.ct / 2.0 + 1'). Find the average of the
# selected rows, and this is the median value.
- cte_table.project(average([extract_epoch(cte_table[column_sym])], "median"))
- .where(
- Arel::Nodes::Between.new(
- cte_table[:row_id],
- Arel::Nodes::And.new(
- [(cte_table[:ct] / Arel.sql('2.0')),
- (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ result =
+ cte_table
+ .project(*median_projections(cte_table, column_sym, partition_column))
+ .where(
+ Arel::Nodes::Between.new(
+ cte_table[:row_id],
+ Arel::Nodes::And.new(
+ [(cte_table[:ct] / Arel.sql('2.0')),
+ (cte_table[:ct] / Arel.sql('2.0') + 1)]
+ )
)
)
- )
- .with(query_so_far, cte)
- .to_sql
+ .with(query_so_far, cte)
+
+ result.group(cte_table[partition_column]).order(cte_table[partition_column]) if partition_column
+
+ result.to_sql
end
private
+ def median_queries(arel_table, query_so_far, column_sym, partition_column = nil)
+ if Gitlab::Database.postgresql?
+ pg_median_datetime_sql(arel_table, query_so_far, column_sym, partition_column)
+ elsif Gitlab::Database.mysql?
+ raise NotSupportedError, "partition_column is not supported for MySQL" if partition_column
+
+ mysql_median_datetime_sql(arel_table, query_so_far, column_sym)
+ end
+ end
+
+ def execute_queries(arel_table, query_so_far, column_sym, partition_column = nil)
+ queries = median_queries(arel_table, query_so_far, column_sym, partition_column)
+
+ Array.wrap(queries).map { |query| ActiveRecord::Base.connection.execute(query) }
+ end
+
def average(args, as)
Arel::Nodes::NamedFunction.new("AVG", args, as)
end
+ def rank_rows(arel_table, column_sym, partition_column)
+ column_row = arel_table[column_sym].as(column_sym.to_s)
+
+ if partition_column
+ partition_row = arel_table[partition_column]
+ row_id =
+ Arel::Nodes::Over.new(
+ Arel::Nodes::NamedFunction.new('rank', []),
+ Arel::Nodes::Window.new.partition(arel_table[partition_column])
+ .order(arel_table[column_sym])
+ ).as('row_id')
+
+ count = arel_table.from(arel_table.alias)
+ .project('COUNT(*)')
+ .where(arel_table[partition_column].eq(arel_table.alias[partition_column]))
+ .as('ct')
+
+ [partition_row, column_row, row_id, count]
+ else
+ row_id =
+ Arel::Nodes::Over.new(
+ Arel::Nodes::NamedFunction.new('row_number', []),
+ Arel::Nodes::Window.new.order(arel_table[column_sym])
+ ).as('row_id')
+
+ count = arel_table.project("COUNT(1)").as('ct')
+
+ [column_row, row_id, count]
+ end
+ end
+
+ def median_projections(table, column_sym, partition_column)
+ projections = []
+ projections << table[partition_column] if partition_column
+ projections << average([extract_epoch(table[column_sym])], "median")
+ projections
+ end
+
def extract_epoch(arel_attribute)
Arel.sql(%Q{EXTRACT(EPOCH FROM "#{arel_attribute.relation.name}"."#{arel_attribute.name}")})
end
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index b2fca2c16de..eabcf46cf58 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -238,9 +238,9 @@ module Gitlab
self.__send__("#{key}=", options[key.to_sym]) # rubocop:disable GitlabSecurity/PublicSend
end
- @loaded_all_data = false
# Retain the actual size before it is encoded
@loaded_size = @data.bytesize if @data
+ @loaded_all_data = @loaded_size == size
end
def binary?
@@ -255,10 +255,15 @@ module Gitlab
# memory as a Ruby string.
def load_all_data!(repository)
return if @data == '' # don't mess with submodule blobs
- return @data if @loaded_all_data
- Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
- @data = begin
+ # Even if we return early, recalculate wether this blob is binary in
+ # case a blob was initialized as text but the full data isn't
+ @binary = nil
+
+ return if @loaded_all_data
+
+ @data = Gitlab::GitalyClient.migrate(:git_blob_load_all_data) do |is_enabled|
+ begin
if is_enabled
repository.gitaly_blob_client.get_blob(oid: id, limit: -1).data
else
@@ -269,7 +274,6 @@ module Gitlab
@loaded_all_data = true
@loaded_size = @data.bytesize
- @binary = nil
end
def name
diff --git a/lib/gitlab/git/branch.rb b/lib/gitlab/git/branch.rb
index ae7e88f0503..6351cfb83e3 100644
--- a/lib/gitlab/git/branch.rb
+++ b/lib/gitlab/git/branch.rb
@@ -1,6 +1,8 @@
module Gitlab
module Git
class Branch < Ref
+ STALE_BRANCH_THRESHOLD = 3.months
+
def self.find(repo, branch_name)
if branch_name.is_a?(Gitlab::Git::Branch)
branch_name
@@ -12,6 +14,18 @@ module Gitlab
def initialize(repository, name, target, target_commit)
super(repository, name, target, target_commit)
end
+
+ def active?
+ self.dereferenced_target.committed_date >= STALE_BRANCH_THRESHOLD.ago
+ end
+
+ def stale?
+ !active?
+ end
+
+ def state
+ active? ? :active : :stale
+ end
end
end
end
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index ae27a138b7c..594b6a9cbc5 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -250,6 +250,45 @@ module Gitlab
end
end
+ def extract_signature_lazily(repository, commit_id)
+ BatchLoader.for({ repository: repository, commit_id: commit_id }).batch do |items, loader|
+ items_by_repo = items.group_by { |i| i[:repository] }
+
+ items_by_repo.each do |repo, items|
+ commit_ids = items.map { |i| i[:commit_id] }
+
+ signatures = batch_signature_extraction(repository, commit_ids)
+
+ signatures.each do |commit_sha, signature_data|
+ loader.call({ repository: repository, commit_id: commit_sha }, signature_data)
+ end
+ end
+ end
+ end
+
+ def batch_signature_extraction(repository, commit_ids)
+ repository.gitaly_migrate(:extract_commit_signature_in_batch) do |is_enabled|
+ if is_enabled
+ gitaly_batch_signature_extraction(repository, commit_ids)
+ else
+ rugged_batch_signature_extraction(repository, commit_ids)
+ end
+ end
+ end
+
+ def gitaly_batch_signature_extraction(repository, commit_ids)
+ repository.gitaly_commit_client.get_commit_signatures(commit_ids)
+ end
+
+ def rugged_batch_signature_extraction(repository, commit_ids)
+ commit_ids.each_with_object({}) do |commit_id, signatures|
+ signature_data = rugged_extract_signature(repository, commit_id)
+ next unless signature_data
+
+ signatures[commit_id] = signature_data
+ end
+ end
+
def rugged_extract_signature(repository, commit_id)
begin
Rugged::Commit.extract_signature(repository.rugged, commit_id)
diff --git a/lib/gitlab/git/lfs_changes.rb b/lib/gitlab/git/lfs_changes.rb
index 48434047fce..b9e5cf258f4 100644
--- a/lib/gitlab/git/lfs_changes.rb
+++ b/lib/gitlab/git/lfs_changes.rb
@@ -7,6 +7,28 @@ module Gitlab
end
def new_pointers(object_limit: nil, not_in: nil)
+ @repository.gitaly_migrate(:blob_get_new_lfs_pointers) do |is_enabled|
+ if is_enabled
+ @repository.gitaly_blob_client.get_new_lfs_pointers(@newrev, object_limit, not_in)
+ else
+ git_new_pointers(object_limit, not_in)
+ end
+ end
+ end
+
+ def all_pointers
+ @repository.gitaly_migrate(:blob_get_all_lfs_pointers) do |is_enabled|
+ if is_enabled
+ @repository.gitaly_blob_client.get_all_lfs_pointers(@newrev)
+ else
+ git_all_pointers
+ end
+ end
+ end
+
+ private
+
+ def git_new_pointers(object_limit, not_in)
@new_pointers ||= begin
rev_list.new_objects(not_in: not_in, require_path: true) do |object_ids|
object_ids = object_ids.take(object_limit) if object_limit
@@ -16,14 +38,12 @@ module Gitlab
end
end
- def all_pointers
+ def git_all_pointers
rev_list.all_objects(require_path: true) do |object_ids|
Gitlab::Git::Blob.batch_lfs_pointers(@repository, object_ids)
end
end
- private
-
def rev_list
Gitlab::Git::RevList.new(@repository, newrev: @newrev)
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index e3cbf017e55..21c79a7a550 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -467,7 +467,8 @@ module Gitlab
follow: false,
skip_merges: false,
after: nil,
- before: nil
+ before: nil,
+ all: false
}
options = default_options.merge(options)
@@ -489,13 +490,16 @@ module Gitlab
# Used in gitaly-ruby
def raw_log(options)
- actual_ref = options[:ref] || root_ref
- begin
- sha = sha_from_ref(actual_ref)
- rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
- # Return an empty array if the ref wasn't found
- return []
- end
+ sha =
+ unless options[:all]
+ actual_ref = options[:ref] || root_ref
+ begin
+ sha_from_ref(actual_ref)
+ rescue Rugged::OdbError, Rugged::InvalidError, Rugged::ReferenceError
+ # Return an empty array if the ref wasn't found
+ return []
+ end
+ end
log_by_shell(sha, options)
end
@@ -1032,6 +1036,21 @@ module Gitlab
end
end
+ def license_short_name
+ gitaly_migrate(:license_short_name) do |is_enabled|
+ if is_enabled
+ gitaly_repository_client.license_short_name
+ else
+ begin
+ # The licensee gem creates a Rugged object from the path:
+ # https://github.com/benbalter/licensee/blob/v8.7.0/lib/licensee/projects/git_project.rb
+ Licensee.license(path).try(:key)
+ rescue Rugged::Error
+ end
+ end
+ end
+ end
+
def with_repo_branch_commit(start_repository, start_branch_name)
Gitlab::Git.check_namespace!(start_repository)
start_repository = RemoteRepository.new(start_repository) unless start_repository.is_a?(RemoteRepository)
@@ -1701,7 +1720,12 @@ module Gitlab
cmd << '--no-merges' if options[:skip_merges]
cmd << "--after=#{options[:after].iso8601}" if options[:after]
cmd << "--before=#{options[:before].iso8601}" if options[:before]
- cmd << sha
+
+ if options[:all]
+ cmd += %w[--all --reverse]
+ else
+ cmd << sha
+ end
# :path can be a string or an array of strings
if options[:path].present?
@@ -1918,7 +1942,16 @@ module Gitlab
cmd << "--before=#{options[:before].iso8601}" if options[:before]
cmd << "--max-count=#{options[:max_count]}" if options[:max_count]
cmd << "--left-right" if options[:left_right]
- cmd += %W[--count #{options[:ref]}]
+ cmd << '--count'
+
+ cmd << if options[:all]
+ '--all'
+ elsif options[:ref]
+ options[:ref]
+ else
+ raise ArgumentError, "Please specify a valid ref or set the 'all' attribute to true"
+ end
+
cmd += %W[-- #{options[:path]}] if options[:path].present?
cmd
end
@@ -2206,7 +2239,7 @@ module Gitlab
with_worktree(squash_path, branch, sparse_checkout_files: diff_files, env: env) do
# Apply diff of the `diff_range` to the worktree
diff = run_git!(%W(diff --binary #{diff_range}))
- run_git!(%w(apply --index), chdir: squash_path, env: env) do |stdin|
+ run_git!(%w(apply --index --whitespace=nowarn), chdir: squash_path, env: env) do |stdin|
stdin.binmode
stdin.write(diff)
end
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index ba6058fd3c9..b6ceb542dd1 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -14,14 +14,14 @@ module Gitlab
# Uses rugged for raw objects
#
# Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/320
- def where(repository, sha, path = nil)
+ def where(repository, sha, path = nil, recursive = false)
path = nil if path == '' || path == '/'
Gitlab::GitalyClient.migrate(:tree_entries) do |is_enabled|
if is_enabled
- repository.gitaly_commit_client.tree_entries(repository, sha, path)
+ repository.gitaly_commit_client.tree_entries(repository, sha, path, recursive)
else
- tree_entries_from_rugged(repository, sha, path)
+ tree_entries_from_rugged(repository, sha, path, recursive)
end
end
end
@@ -57,7 +57,22 @@ module Gitlab
end
end
- def tree_entries_from_rugged(repository, sha, path)
+ def tree_entries_from_rugged(repository, sha, path, recursive)
+ current_path_entries = get_tree_entries_from_rugged(repository, sha, path)
+ ordered_entries = []
+
+ current_path_entries.each do |entry|
+ ordered_entries << entry
+
+ if recursive && entry.dir?
+ ordered_entries.concat(tree_entries_from_rugged(repository, sha, entry.path, true))
+ end
+ end
+
+ ordered_entries
+ end
+
+ def get_tree_entries_from_rugged(repository, sha, path)
commit = repository.lookup(sha)
root_tree = commit.tree
diff --git a/lib/gitlab/git/wiki.rb b/lib/gitlab/git/wiki.rb
index ac12271a87e..52b44b9b3c5 100644
--- a/lib/gitlab/git/wiki.rb
+++ b/lib/gitlab/git/wiki.rb
@@ -59,7 +59,7 @@ module Gitlab
end
def pages(limit: nil)
- @repository.gitaly_migrate(:wiki_get_all_pages, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_get_all_pages) do |is_enabled|
if is_enabled
gitaly_get_all_pages
else
@@ -68,9 +68,8 @@ module Gitlab
end
end
- # Disable because of https://gitlab.com/gitlab-org/gitlab-ce/issues/42039
def page(title:, version: nil, dir: nil)
- @repository.gitaly_migrate(:wiki_find_page, status: Gitlab::GitalyClient::MigrationStatus::DISABLED) do |is_enabled|
+ @repository.gitaly_migrate(:wiki_find_page) do |is_enabled|
if is_enabled
gitaly_find_page(title: title, version: version, dir: dir)
else
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index bbdb593d4e2..6400089a22f 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -199,7 +199,7 @@ module Gitlab
def check_repository_existence!
unless repository.exists?
- raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
+ raise NotFoundError, ERROR_MESSAGES[:no_repo]
end
end
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index c5d3e944f7d..9cd76630484 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -125,6 +125,8 @@ module Gitlab
kwargs = yield(kwargs) if block_given?
stub(service, storage).__send__(rpc, request, kwargs) # rubocop:disable GitlabSecurity/PublicSend
+ rescue GRPC::Unavailable => ex
+ handle_grpc_unavailable!(ex)
ensure
duration = Gitlab::Metrics::System.monotonic_time - start
@@ -135,6 +137,27 @@ module Gitlab
duration)
end
+ def self.handle_grpc_unavailable!(ex)
+ status = ex.to_status
+ raise ex unless status.details == 'Endpoint read failed'
+
+ # There is a bug in grpc 1.8.x that causes a client process to get stuck
+ # always raising '14:Endpoint read failed'. The only thing that we can
+ # do to recover is to restart the process.
+ #
+ # See https://gitlab.com/gitlab-org/gitaly/issues/1029
+
+ if Sidekiq.server?
+ raise Gitlab::SidekiqMiddleware::Shutdown::WantShutdown.new(ex.to_s)
+ else
+ # SIGQUIT requests a Unicorn worker to shut down gracefully after the current request.
+ Process.kill('QUIT', Process.pid)
+ end
+
+ raise ex
+ end
+ private_class_method :handle_grpc_unavailable!
+
def self.current_transaction_labels
Gitlab::Metrics::Transaction.current&.labels || {}
end
diff --git a/lib/gitlab/gitaly_client/blob_service.rb b/lib/gitlab/gitaly_client/blob_service.rb
index dfa0fa43b0f..28554208984 100644
--- a/lib/gitlab/gitaly_client/blob_service.rb
+++ b/lib/gitlab/gitaly_client/blob_service.rb
@@ -45,16 +45,7 @@ module Gitlab
response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_lfs_pointers, request)
- response.flat_map do |message|
- message.lfs_pointers.map do |lfs_pointer|
- Gitlab::Git::Blob.new(
- id: lfs_pointer.oid,
- size: lfs_pointer.size,
- data: lfs_pointer.data,
- binary: Gitlab::Git::Blob.binary?(lfs_pointer.data)
- )
- end
- end
+ map_lfs_pointers(response)
end
def get_blobs(revision_paths, limit = -1)
@@ -80,6 +71,50 @@ module Gitlab
GitalyClient::BlobsStitcher.new(response)
end
+
+ def get_new_lfs_pointers(revision, limit, not_in)
+ request = Gitaly::GetNewLFSPointersRequest.new(
+ repository: @gitaly_repo,
+ revision: encode_binary(revision),
+ limit: limit || 0
+ )
+
+ if not_in.nil? || not_in == :all
+ request.not_in_all = true
+ else
+ request.not_in_refs += not_in
+ end
+
+ response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_new_lfs_pointers, request)
+
+ map_lfs_pointers(response)
+ end
+
+ def get_all_lfs_pointers(revision)
+ request = Gitaly::GetNewLFSPointersRequest.new(
+ repository: @gitaly_repo,
+ revision: encode_binary(revision)
+ )
+
+ response = GitalyClient.call(@gitaly_repo.storage_name, :blob_service, :get_all_lfs_pointers, request)
+
+ map_lfs_pointers(response)
+ end
+
+ private
+
+ def map_lfs_pointers(response)
+ response.flat_map do |message|
+ message.lfs_pointers.map do |lfs_pointer|
+ Gitlab::Git::Blob.new(
+ id: lfs_pointer.oid,
+ size: lfs_pointer.size,
+ data: lfs_pointer.data,
+ binary: Gitlab::Git::Blob.binary?(lfs_pointer.data)
+ )
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/gitaly_client/commit_service.rb b/lib/gitlab/gitaly_client/commit_service.rb
index 269a048cf5d..456a8a1a2d6 100644
--- a/lib/gitlab/gitaly_client/commit_service.rb
+++ b/lib/gitlab/gitaly_client/commit_service.rb
@@ -105,11 +105,12 @@ module Gitlab
entry unless entry.oid.blank?
end
- def tree_entries(repository, revision, path)
+ def tree_entries(repository, revision, path, recursive)
request = Gitaly::GetTreeEntriesRequest.new(
repository: @gitaly_repo,
revision: encode_binary(revision),
- path: path.present? ? encode_binary(path) : '.'
+ path: path.present? ? encode_binary(path) : '.',
+ recursive: recursive
)
response = GitalyClient.call(@repository.storage, :commit_service, :get_tree_entries, request, timeout: GitalyClient.medium_timeout)
@@ -133,7 +134,8 @@ module Gitlab
def commit_count(ref, options = {})
request = Gitaly::CountCommitsRequest.new(
repository: @gitaly_repo,
- revision: encode_binary(ref)
+ revision: encode_binary(ref),
+ all: !!options[:all]
)
request.after = Google::Protobuf::Timestamp.new(seconds: options[:after].to_i) if options[:after].present?
request.before = Google::Protobuf::Timestamp.new(seconds: options[:before].to_i) if options[:before].present?
@@ -268,6 +270,7 @@ module Gitlab
offset: options[:offset],
follow: options[:follow],
skip_merges: options[:skip_merges],
+ all: !!options[:all],
disable_walk: true # This option is deprecated. The 'walk' implementation is being removed.
)
request.after = GitalyClient.timestamp(options[:after]) if options[:after]
@@ -318,6 +321,23 @@ module Gitlab
[signature, signed_text]
end
+ def get_commit_signatures(commit_ids)
+ request = Gitaly::GetCommitSignaturesRequest.new(repository: @gitaly_repo, commit_ids: commit_ids)
+ response = GitalyClient.call(@repository.storage, :commit_service, :get_commit_signatures, request)
+
+ signatures = Hash.new { |h, k| h[k] = [''.b, ''.b] }
+ current_commit_id = nil
+
+ response.each do |message|
+ current_commit_id = message.commit_id if message.commit_id.present?
+
+ signatures[current_commit_id].first << message.signature
+ signatures[current_commit_id].last << message.signed_text
+ end
+
+ signatures
+ end
+
private
def call_commit_diff(request_params, options = {})
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
index 603457d0664..fdb3247cf4d 100644
--- a/lib/gitlab/gitaly_client/repository_service.rb
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -41,7 +41,7 @@ module Gitlab
end
def apply_gitattributes(revision)
- request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: revision)
+ request = Gitaly::ApplyGitattributesRequest.new(repository: @gitaly_repo, revision: encode_binary(revision))
GitalyClient.call(@storage, :repository_service, :apply_gitattributes, request)
end
@@ -249,6 +249,14 @@ module Gitlab
raise Gitlab::Git::OSError.new(response.error) unless response.error.empty?
end
+
+ def license_short_name
+ request = Gitaly::FindLicenseRequest.new(repository: @gitaly_repo)
+
+ response = GitalyClient.call(@storage, :repository_service, :find_license, request, timeout: GitalyClient.fast_timeout)
+
+ response.license_short_name.presence
+ end
end
end
end
diff --git a/lib/gitlab/gon_helper.rb b/lib/gitlab/gon_helper.rb
index ba04387022d..a7e055ac444 100644
--- a/lib/gitlab/gon_helper.rb
+++ b/lib/gitlab/gon_helper.rb
@@ -19,6 +19,8 @@ module Gitlab
gon.gitlab_logo = ActionController::Base.helpers.asset_path('gitlab_logo.png')
gon.sprite_icons = IconsHelper.sprite_icon_path
gon.sprite_file_icons = IconsHelper.sprite_file_icons_path
+ gon.test_env = Rails.env.test?
+ gon.suggested_label_colors = LabelsHelper.suggested_colors
if current_user
gon.current_user_id = current_user.id
diff --git a/lib/gitlab/gpg/commit.rb b/lib/gitlab/gpg/commit.rb
index 90dd569aaf8..6d2278d0876 100644
--- a/lib/gitlab/gpg/commit.rb
+++ b/lib/gitlab/gpg/commit.rb
@@ -1,15 +1,29 @@
module Gitlab
module Gpg
class Commit
+ include Gitlab::Utils::StrongMemoize
+
def initialize(commit)
@commit = commit
repo = commit.project.repository.raw_repository
- @signature_text, @signed_text = Gitlab::Git::Commit.extract_signature(repo, commit.sha)
+ @signature_data = Gitlab::Git::Commit.extract_signature_lazily(repo, commit.sha || commit.id)
+ end
+
+ def signature_text
+ strong_memoize(:signature_text) do
+ @signature_data&.itself && @signature_data[0]
+ end
+ end
+
+ def signed_text
+ strong_memoize(:signed_text) do
+ @signature_data&.itself && @signature_data[1]
+ end
end
def has_signature?
- !!(@signature_text && @signed_text)
+ !!(signature_text && signed_text)
end
def signature
@@ -53,7 +67,7 @@ module Gitlab
end
def verified_signature
- @verified_signature ||= GPGME::Crypto.new.verify(@signature_text, signed_text: @signed_text) do |verified_signature|
+ @verified_signature ||= GPGME::Crypto.new.verify(signature_text, signed_text: signed_text) do |verified_signature|
break verified_signature
end
end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index af203ff711d..b713fa7e1cd 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -3,7 +3,7 @@ module Gitlab
extend self
# For every version update, the version history in import_export.md has to be kept up to date.
- VERSION = '0.2.2'.freeze
+ VERSION = '0.2.3'.freeze
FILENAME_LIMIT = 50
def export_path(relative_path:)
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 9f404003125..4bdd01f5e94 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -65,6 +65,7 @@ project_tree:
- :create_access_levels
- :project_feature
- :custom_attributes
+ - :project_badges
# Only include the following attributes for the models specified.
included_attributes:
@@ -125,6 +126,8 @@ excluded_attributes:
- :when
push_event_payload:
- :event_id
+ project_badges:
+ - :group_id
methods:
labels:
@@ -147,3 +150,5 @@ methods:
- :action
push_event_payload:
- :action
+ project_badges:
+ - :type
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 759833a5ee5..cf6b7e306dd 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -16,7 +16,8 @@ module Gitlab
priorities: :label_priorities,
auto_devops: :project_auto_devops,
label: :project_label,
- custom_attributes: 'ProjectCustomAttribute' }.freeze
+ custom_attributes: 'ProjectCustomAttribute',
+ project_badges: 'Badge' }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
diff --git a/lib/gitlab/job_waiter.rb b/lib/gitlab/job_waiter.rb
index f654508c391..f7a8eae0be4 100644
--- a/lib/gitlab/job_waiter.rb
+++ b/lib/gitlab/job_waiter.rb
@@ -15,16 +15,22 @@ module Gitlab
# push to that array when done. Once the waiter has popped `count` items, it
# knows all the jobs are done.
class JobWaiter
+ KEY_PREFIX = "gitlab:job_waiter".freeze
+
def self.notify(key, jid)
Gitlab::Redis::SharedState.with { |redis| redis.lpush(key, jid) }
end
+ def self.key?(key)
+ key.is_a?(String) && key =~ /\A#{KEY_PREFIX}:\h{8}-\h{4}-\h{4}-\h{4}-\h{12}\z/
+ end
+
attr_reader :key, :finished
attr_accessor :jobs_remaining
# jobs_remaining - the number of jobs left to wait for
# key - The key of this waiter.
- def initialize(jobs_remaining = 0, key = "gitlab:job_waiter:#{SecureRandom.uuid}")
+ def initialize(jobs_remaining = 0, key = "#{KEY_PREFIX}:#{SecureRandom.uuid}")
@key = key
@jobs_remaining = jobs_remaining
@finished = []
diff --git a/lib/gitlab/kubernetes/config_map.rb b/lib/gitlab/kubernetes/config_map.rb
new file mode 100644
index 00000000000..95e1054919d
--- /dev/null
+++ b/lib/gitlab/kubernetes/config_map.rb
@@ -0,0 +1,37 @@
+module Gitlab
+ module Kubernetes
+ class ConfigMap
+ def initialize(name, values)
+ @name = name
+ @values = values
+ end
+
+ def generate
+ resource = ::Kubeclient::Resource.new
+ resource.metadata = metadata
+ resource.data = { values: values }
+ resource
+ end
+
+ private
+
+ attr_reader :name, :values
+
+ def metadata
+ {
+ name: config_map_name,
+ namespace: namespace,
+ labels: { name: config_map_name }
+ }
+ end
+
+ def config_map_name
+ "values-content-configuration-#{name}"
+ end
+
+ def namespace
+ Gitlab::Kubernetes::Helm::NAMESPACE
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/api.rb b/lib/gitlab/kubernetes/helm/api.rb
index 737081ddc5b..2edd34109ba 100644
--- a/lib/gitlab/kubernetes/helm/api.rb
+++ b/lib/gitlab/kubernetes/helm/api.rb
@@ -9,7 +9,8 @@ module Gitlab
def install(command)
@namespace.ensure_exists!
- @kubeclient.create_pod(pod_resource(command))
+ create_config_map(command) if command.config_map?
+ @kubeclient.create_pod(command.pod_resource)
end
##
@@ -33,8 +34,10 @@ module Gitlab
private
- def pod_resource(command)
- Gitlab::Kubernetes::Helm::Pod.new(command, @namespace.name, @kubeclient).generate
+ def create_config_map(command)
+ command.config_map_resource.tap do |config_map_resource|
+ @kubeclient.create_config_map(config_map_resource)
+ end
end
end
end
diff --git a/lib/gitlab/kubernetes/helm/base_command.rb b/lib/gitlab/kubernetes/helm/base_command.rb
new file mode 100644
index 00000000000..6e4df05aa7e
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/base_command.rb
@@ -0,0 +1,40 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class BaseCommand
+ attr_reader :name
+
+ def initialize(name)
+ @name = name
+ end
+
+ def pod_resource
+ Gitlab::Kubernetes::Helm::Pod.new(self, namespace).generate
+ end
+
+ def generate_script
+ <<~HEREDOC
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ HEREDOC
+ end
+
+ def config_map?
+ false
+ end
+
+ def pod_name
+ "install-#{name}"
+ end
+
+ private
+
+ def namespace
+ Gitlab::Kubernetes::Helm::NAMESPACE
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/init_command.rb b/lib/gitlab/kubernetes/helm/init_command.rb
new file mode 100644
index 00000000000..a02e64561f6
--- /dev/null
+++ b/lib/gitlab/kubernetes/helm/init_command.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module Kubernetes
+ module Helm
+ class InitCommand < BaseCommand
+ def generate_script
+ super + [
+ init_helm_command
+ ].join("\n")
+ end
+
+ private
+
+ def init_helm_command
+ "helm init >/dev/null"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/kubernetes/helm/install_command.rb b/lib/gitlab/kubernetes/helm/install_command.rb
index bf6981035f4..30af3e97b4a 100644
--- a/lib/gitlab/kubernetes/helm/install_command.rb
+++ b/lib/gitlab/kubernetes/helm/install_command.rb
@@ -1,54 +1,45 @@
module Gitlab
module Kubernetes
module Helm
- class InstallCommand
- attr_reader :name, :install_helm, :chart, :chart_values_file
+ class InstallCommand < BaseCommand
+ attr_reader :name, :chart, :repository, :values
- def initialize(name, install_helm: false, chart: false, chart_values_file: false)
+ def initialize(name, chart:, values:, repository: nil)
@name = name
- @install_helm = install_helm
@chart = chart
- @chart_values_file = chart_values_file
+ @values = values
+ @repository = repository
end
- def pod_name
- "install-#{name}"
+ def generate_script
+ super + [
+ init_command,
+ repository_command,
+ script_command
+ ].compact.join("\n")
end
- def generate_script(namespace_name)
- [
- install_dps_command,
- init_command,
- complete_command(namespace_name)
- ].join("\n")
+ def config_map?
+ true
+ end
+
+ def config_map_resource
+ Gitlab::Kubernetes::ConfigMap.new(name, values).generate
end
private
def init_command
- if install_helm
- 'helm init >/dev/null'
- else
- 'helm init --client-only >/dev/null'
- end
+ 'helm init --client-only >/dev/null'
end
- def complete_command(namespace_name)
- return unless chart
-
- if chart_values_file
- "helm install #{chart} --name #{name} --namespace #{namespace_name} -f /data/helm/#{name}/config/values.yaml >/dev/null"
- else
- "helm install #{chart} --name #{name} --namespace #{namespace_name} >/dev/null"
- end
+ def repository_command
+ "helm repo add #{name} #{repository}" if repository
end
- def install_dps_command
+ def script_command
<<~HEREDOC
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{Gitlab::Kubernetes::Helm::HELM_VERSION}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
+ helm install #{chart} --name #{name} --namespace #{Gitlab::Kubernetes::Helm::NAMESPACE} -f /data/helm/#{name}/config/values.yaml >/dev/null
HEREDOC
end
end
diff --git a/lib/gitlab/kubernetes/helm/pod.rb b/lib/gitlab/kubernetes/helm/pod.rb
index ca5e06009fa..1e12299eefd 100644
--- a/lib/gitlab/kubernetes/helm/pod.rb
+++ b/lib/gitlab/kubernetes/helm/pod.rb
@@ -2,18 +2,17 @@ module Gitlab
module Kubernetes
module Helm
class Pod
- def initialize(command, namespace_name, kubeclient)
+ def initialize(command, namespace_name)
@command = command
@namespace_name = namespace_name
- @kubeclient = kubeclient
end
def generate
spec = { containers: [container_specification], restartPolicy: 'Never' }
- if command.chart_values_file
- create_config_map
+ if command.config_map?
spec[:volumes] = volumes_specification
+ spec[:containers][0][:volumeMounts] = volume_mounts_specification
end
::Kubeclient::Resource.new(metadata: metadata, spec: spec)
@@ -21,18 +20,16 @@ module Gitlab
private
- attr_reader :command, :namespace_name, :kubeclient
+ attr_reader :command, :namespace_name, :kubeclient, :config_map
def container_specification
- container = {
+ {
name: 'helm',
image: 'alpine:3.6',
env: generate_pod_env(command),
command: %w(/bin/sh),
args: %w(-c $(COMMAND_SCRIPT))
}
- container[:volumeMounts] = volume_mounts_specification if command.chart_values_file
- container
end
def labels
@@ -50,13 +47,12 @@ module Gitlab
}
end
- def volume_mounts_specification
- [
- {
- name: 'configuration-volume',
- mountPath: "/data/helm/#{command.name}/config"
- }
- ]
+ def generate_pod_env(command)
+ {
+ HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION,
+ TILLER_NAMESPACE: namespace_name,
+ COMMAND_SCRIPT: command.generate_script
+ }.map { |key, value| { name: key, value: value } }
end
def volumes_specification
@@ -71,23 +67,13 @@ module Gitlab
]
end
- def generate_pod_env(command)
- {
- HELM_VERSION: Gitlab::Kubernetes::Helm::HELM_VERSION,
- TILLER_NAMESPACE: namespace_name,
- COMMAND_SCRIPT: command.generate_script(namespace_name)
- }.map { |key, value| { name: key, value: value } }
- end
-
- def create_config_map
- resource = ::Kubeclient::Resource.new
- resource.metadata = {
- name: "values-content-configuration-#{command.name}",
- namespace: namespace_name,
- labels: { name: "values-content-configuration-#{command.name}" }
- }
- resource.data = { values: File.read(command.chart_values_file) }
- kubeclient.create_config_map(resource)
+ def volume_mounts_specification
+ [
+ {
+ name: 'configuration-volume',
+ mountPath: "/data/helm/#{command.name}/config"
+ }
+ ]
end
end
end
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
deleted file mode 100644
index e60ceba27c8..00000000000
--- a/lib/gitlab/ldap/access.rb
+++ /dev/null
@@ -1,87 +0,0 @@
-# LDAP authorization model
-#
-# * Check if we are allowed access (not blocked)
-#
-module Gitlab
- module LDAP
- class Access
- attr_reader :provider, :user
-
- def self.open(user, &block)
- Gitlab::LDAP::Adapter.open(user.ldap_identity.provider) do |adapter|
- block.call(self.new(user, adapter))
- end
- end
-
- def self.allowed?(user)
- self.open(user) do |access|
- if access.allowed?
- Users::UpdateService.new(user, user: user, last_credential_check_at: Time.now).execute
-
- true
- else
- false
- end
- end
- end
-
- def initialize(user, adapter = nil)
- @adapter = adapter
- @user = user
- @provider = user.ldap_identity.provider
- end
-
- def allowed?
- if ldap_user
- unless ldap_config.active_directory
- unblock_user(user, 'is available again') if user.ldap_blocked?
- return true
- end
-
- # Block user in GitLab if he/she was blocked in AD
- if Gitlab::LDAP::Person.disabled_via_active_directory?(user.ldap_identity.extern_uid, adapter)
- block_user(user, 'is disabled in Active Directory')
- false
- else
- unblock_user(user, 'is not disabled anymore') if user.ldap_blocked?
- true
- end
- else
- # Block the user if they no longer exist in LDAP/AD
- block_user(user, 'does not exist anymore')
- false
- end
- end
-
- def adapter
- @adapter ||= Gitlab::LDAP::Adapter.new(provider)
- end
-
- def ldap_config
- Gitlab::LDAP::Config.new(provider)
- end
-
- def ldap_user
- @ldap_user ||= Gitlab::LDAP::Person.find_by_dn(user.ldap_identity.extern_uid, adapter)
- end
-
- def block_user(user, reason)
- user.ldap_block
-
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "blocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
- end
-
- def unblock_user(user, reason)
- user.activate
-
- Gitlab::AppLogger.info(
- "LDAP account \"#{user.ldap_identity.extern_uid}\" #{reason}, " \
- "unblocking Gitlab user \"#{user.name}\" (#{user.email})"
- )
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
deleted file mode 100644
index 76863e77dc3..00000000000
--- a/lib/gitlab/ldap/adapter.rb
+++ /dev/null
@@ -1,108 +0,0 @@
-module Gitlab
- module LDAP
- class Adapter
- attr_reader :provider, :ldap
-
- def self.open(provider, &block)
- Net::LDAP.open(config(provider).adapter_options) do |ldap|
- block.call(self.new(provider, ldap))
- end
- end
-
- def self.config(provider)
- Gitlab::LDAP::Config.new(provider)
- end
-
- def initialize(provider, ldap = nil)
- @provider = provider
- @ldap = ldap || Net::LDAP.new(config.adapter_options)
- end
-
- def config
- Gitlab::LDAP::Config.new(provider)
- end
-
- def users(fields, value, limit = nil)
- options = user_options(Array(fields), value, limit)
-
- entries = ldap_search(options).select do |entry|
- entry.respond_to? config.uid
- end
-
- entries.map do |entry|
- Gitlab::LDAP::Person.new(entry, provider)
- end
- end
-
- def user(*args)
- users(*args).first
- end
-
- def dn_matches_filter?(dn, filter)
- ldap_search(base: dn,
- filter: filter,
- scope: Net::LDAP::SearchScope_BaseObject,
- attributes: %w{dn}).any?
- end
-
- def ldap_search(*args)
- # Net::LDAP's `time` argument doesn't work. Use Ruby `Timeout` instead.
- Timeout.timeout(config.timeout) do
- results = ldap.search(*args)
-
- if results.nil?
- response = ldap.get_operation_result
-
- unless response.code.zero?
- Rails.logger.warn("LDAP search error: #{response.message}")
- end
-
- []
- else
- results
- end
- end
- rescue Net::LDAP::Error => error
- Rails.logger.warn("LDAP search raised exception #{error.class}: #{error.message}")
- []
- rescue Timeout::Error
- Rails.logger.warn("LDAP search timed out after #{config.timeout} seconds")
- []
- end
-
- private
-
- def user_options(fields, value, limit)
- options = {
- attributes: Gitlab::LDAP::Person.ldap_attributes(config),
- base: config.base
- }
-
- options[:size] = limit if limit
-
- if fields.include?('dn')
- raise ArgumentError, 'It is not currently possible to search the DN and other fields at the same time.' if fields.size > 1
-
- options[:base] = value
- options[:scope] = Net::LDAP::SearchScope_BaseObject
- else
- filter = fields.map { |field| Net::LDAP::Filter.eq(field, value) }.inject(:|)
- end
-
- options.merge(filter: user_filter(filter))
- end
-
- def user_filter(filter = nil)
- user_filter = config.constructed_user_filter if config.user_filter.present?
-
- if user_filter && filter
- Net::LDAP::Filter.join(filter, user_filter)
- elsif user_filter
- user_filter
- else
- filter
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/auth_hash.rb b/lib/gitlab/ldap/auth_hash.rb
deleted file mode 100644
index 96171dc26c4..00000000000
--- a/lib/gitlab/ldap/auth_hash.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-# Class to parse and transform the info provided by omniauth
-#
-module Gitlab
- module LDAP
- class AuthHash < Gitlab::OAuth::AuthHash
- def uid
- @uid ||= Gitlab::LDAP::Person.normalize_dn(super)
- end
-
- def username
- super.tap do |username|
- username.downcase! if ldap_config.lowercase_usernames
- end
- end
-
- private
-
- def get_info(key)
- attributes = ldap_config.attributes[key.to_s]
- return super unless attributes
-
- attributes = Array(attributes)
-
- value = nil
- attributes.each do |attribute|
- value = get_raw(attribute)
- value = value.first if value
- break if value.present?
- end
-
- return super unless value
-
- Gitlab::Utils.force_utf8(value)
- value
- end
-
- def get_raw(key)
- auth_hash.extra[:raw_info][key] if auth_hash.extra
- end
-
- def ldap_config
- @ldap_config ||= Gitlab::LDAP::Config.new(self.provider)
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/authentication.rb b/lib/gitlab/ldap/authentication.rb
deleted file mode 100644
index 7274d1c3b43..00000000000
--- a/lib/gitlab/ldap/authentication.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# These calls help to authenticate to LDAP by providing username and password
-#
-# Since multiple LDAP servers are supported, it will loop through all of them
-# until a valid bind is found
-#
-
-module Gitlab
- module LDAP
- class Authentication
- def self.login(login, password)
- return unless Gitlab::LDAP::Config.enabled?
- return unless login.present? && password.present?
-
- auth = nil
- # loop through providers until valid bind
- providers.find do |provider|
- auth = new(provider)
- auth.login(login, password) # true will exit the loop
- end
-
- # If (login, password) was invalid for all providers, the value of auth is now the last
- # Gitlab::LDAP::Authentication instance we tried.
- auth.user
- end
-
- def self.providers
- Gitlab::LDAP::Config.providers
- end
-
- attr_accessor :provider, :ldap_user
-
- def initialize(provider)
- @provider = provider
- end
-
- def login(login, password)
- @ldap_user = adapter.bind_as(
- filter: user_filter(login),
- size: 1,
- password: password
- )
- end
-
- def adapter
- OmniAuth::LDAP::Adaptor.new(config.omniauth_options)
- end
-
- def config
- Gitlab::LDAP::Config.new(provider)
- end
-
- def user_filter(login)
- filter = Net::LDAP::Filter.equals(config.uid, login)
-
- # Apply LDAP user filter if present
- if config.user_filter.present?
- filter = Net::LDAP::Filter.join(filter, config.constructed_user_filter)
- end
-
- filter
- end
-
- def user
- return nil unless ldap_user
-
- Gitlab::LDAP::User.find_by_uid_and_provider(ldap_user.dn, provider)
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/config.rb b/lib/gitlab/ldap/config.rb
deleted file mode 100644
index a6bea98d631..00000000000
--- a/lib/gitlab/ldap/config.rb
+++ /dev/null
@@ -1,235 +0,0 @@
-# Load a specific server configuration
-module Gitlab
- module LDAP
- class Config
- NET_LDAP_ENCRYPTION_METHOD = {
- simple_tls: :simple_tls,
- start_tls: :start_tls,
- plain: nil
- }.freeze
-
- attr_accessor :provider, :options
-
- def self.enabled?
- Gitlab.config.ldap.enabled
- end
-
- def self.servers
- Gitlab.config.ldap['servers']&.values || []
- end
-
- def self.available_servers
- return [] unless enabled?
-
- Array.wrap(servers.first)
- end
-
- def self.providers
- servers.map { |server| server['provider_name'] }
- end
-
- def self.valid_provider?(provider)
- providers.include?(provider)
- end
-
- def self.invalid_provider(provider)
- raise "Unknown provider (#{provider}). Available providers: #{providers}"
- end
-
- def initialize(provider)
- if self.class.valid_provider?(provider)
- @provider = provider
- else
- self.class.invalid_provider(provider)
- end
-
- @options = config_for(@provider) # Use @provider, not provider
- end
-
- def enabled?
- base_config.enabled
- end
-
- def adapter_options
- opts = base_options.merge(
- encryption: encryption_options
- )
-
- opts.merge!(auth_options) if has_auth?
-
- opts
- end
-
- def omniauth_options
- opts = base_options.merge(
- base: base,
- encryption: options['encryption'],
- filter: omniauth_user_filter,
- name_proc: name_proc,
- disable_verify_certificates: !options['verify_certificates']
- )
-
- if has_auth?
- opts.merge!(
- bind_dn: options['bind_dn'],
- password: options['password']
- )
- end
-
- opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
- opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
-
- opts
- end
-
- def base
- options['base']
- end
-
- def uid
- options['uid']
- end
-
- def sync_ssh_keys?
- sync_ssh_keys.present?
- end
-
- # The LDAP attribute in which the ssh keys are stored
- def sync_ssh_keys
- options['sync_ssh_keys']
- end
-
- def user_filter
- options['user_filter']
- end
-
- def constructed_user_filter
- @constructed_user_filter ||= Net::LDAP::Filter.construct(user_filter)
- end
-
- def group_base
- options['group_base']
- end
-
- def admin_group
- options['admin_group']
- end
-
- def active_directory
- options['active_directory']
- end
-
- def block_auto_created_users
- options['block_auto_created_users']
- end
-
- def attributes
- default_attributes.merge(options['attributes'])
- end
-
- def timeout
- options['timeout'].to_i
- end
-
- def has_auth?
- options['password'] || options['bind_dn']
- end
-
- def allow_username_or_email_login
- options['allow_username_or_email_login']
- end
-
- def lowercase_usernames
- options['lowercase_usernames']
- end
-
- def name_proc
- if allow_username_or_email_login
- proc { |name| name.gsub(/@.*\z/, '') }
- else
- proc { |name| name }
- end
- end
-
- def default_attributes
- {
- 'username' => %w(uid sAMAccountName userid),
- 'email' => %w(mail email userPrincipalName),
- 'name' => 'cn',
- 'first_name' => 'givenName',
- 'last_name' => 'sn'
- }
- end
-
- protected
-
- def base_options
- {
- host: options['host'],
- port: options['port']
- }
- end
-
- def base_config
- Gitlab.config.ldap
- end
-
- def config_for(provider)
- base_config.servers.values.find { |server| server['provider_name'] == provider }
- end
-
- def encryption_options
- method = translate_method(options['encryption'])
- return nil unless method
-
- {
- method: method,
- tls_options: tls_options(method)
- }
- end
-
- def translate_method(method_from_config)
- NET_LDAP_ENCRYPTION_METHOD[method_from_config.to_sym]
- end
-
- def tls_options(method)
- return { verify_mode: OpenSSL::SSL::VERIFY_NONE } unless method
-
- opts = if options['verify_certificates']
- OpenSSL::SSL::SSLContext::DEFAULT_PARAMS
- else
- # It is important to explicitly set verify_mode for two reasons:
- # 1. The behavior of OpenSSL is undefined when verify_mode is not set.
- # 2. The net-ldap gem implementation verifies the certificate hostname
- # unless verify_mode is set to VERIFY_NONE.
- { verify_mode: OpenSSL::SSL::VERIFY_NONE }
- end
-
- opts[:ca_file] = options['ca_file'] if options['ca_file'].present?
- opts[:ssl_version] = options['ssl_version'] if options['ssl_version'].present?
-
- opts
- end
-
- def auth_options
- {
- auth: {
- method: :simple,
- username: options['bind_dn'],
- password: options['password']
- }
- }
- end
-
- def omniauth_user_filter
- uid_filter = Net::LDAP::Filter.eq(uid, '%{username}')
-
- if user_filter.present?
- Net::LDAP::Filter.join(uid_filter, constructed_user_filter).to_s
- else
- uid_filter.to_s
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/dn.rb b/lib/gitlab/ldap/dn.rb
deleted file mode 100644
index d6142dc6549..00000000000
--- a/lib/gitlab/ldap/dn.rb
+++ /dev/null
@@ -1,301 +0,0 @@
-# -*- ruby encoding: utf-8 -*-
-
-# Based on the `ruby-net-ldap` gem's `Net::LDAP::DN`
-#
-# For our purposes, this class is used to normalize DNs in order to allow proper
-# comparison.
-#
-# E.g. DNs should be compared case-insensitively (in basically all LDAP
-# implementations or setups), therefore we downcase every DN.
-
-##
-# Objects of this class represent an LDAP DN ("Distinguished Name"). A DN
-# ("Distinguished Name") is a unique identifier for an entry within an LDAP
-# directory. It is made up of a number of other attributes strung together,
-# to identify the entry in the tree.
-#
-# Each attribute that makes up a DN needs to have its value escaped so that
-# the DN is valid. This class helps take care of that.
-#
-# A fully escaped DN needs to be unescaped when analysing its contents. This
-# class also helps take care of that.
-module Gitlab
- module LDAP
- class DN
- FormatError = Class.new(StandardError)
- MalformedError = Class.new(FormatError)
- UnsupportedError = Class.new(FormatError)
-
- def self.normalize_value(given_value)
- dummy_dn = "placeholder=#{given_value}"
- normalized_dn = new(*dummy_dn).to_normalized_s
- normalized_dn.sub(/\Aplaceholder=/, '')
- end
-
- ##
- # Initialize a DN, escaping as required. Pass in attributes in name/value
- # pairs. If there is a left over argument, it will be appended to the dn
- # without escaping (useful for a base string).
- #
- # Most uses of this class will be to escape a DN, rather than to parse it,
- # so storing the dn as an escaped String and parsing parts as required
- # with a state machine seems sensible.
- def initialize(*args)
- if args.length > 1
- initialize_array(args)
- else
- initialize_string(args[0])
- end
- end
-
- ##
- # Parse a DN into key value pairs using ASN from
- # http://tools.ietf.org/html/rfc2253 section 3.
- # rubocop:disable Metrics/AbcSize
- # rubocop:disable Metrics/CyclomaticComplexity
- # rubocop:disable Metrics/PerceivedComplexity
- def each_pair
- state = :key
- key = StringIO.new
- value = StringIO.new
- hex_buffer = ""
-
- @dn.each_char.with_index do |char, dn_index|
- case state
- when :key then
- case char
- when 'a'..'z', 'A'..'Z' then
- state = :key_normal
- key << char
- when '0'..'9' then
- state = :key_oid
- key << char
- when ' ' then state = :key
- else raise(MalformedError, "Unrecognized first character of an RDN attribute type name \"#{char}\"")
- end
- when :key_normal then
- case char
- when '=' then state = :value
- when 'a'..'z', 'A'..'Z', '0'..'9', '-', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN attribute type name character \"#{char}\"")
- end
- when :key_oid then
- case char
- when '=' then state = :value
- when '0'..'9', '.', ' ' then key << char
- else raise(MalformedError, "Unrecognized RDN OID attribute type name character \"#{char}\"")
- end
- when :value then
- case char
- when '\\' then state = :value_normal_escape
- when '"' then state = :value_quoted
- when ' ' then state = :value
- when '#' then
- state = :value_hexstring
- value << char
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else
- state = :value_normal
- value << char
- end
- when :value_normal then
- case char
- when '\\' then state = :value_normal_escape
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- when '+' then raise(UnsupportedError, "Multivalued RDNs are not supported")
- else value << char
- end
- when :value_normal_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal_escape_hex
- hex_buffer = char
- else
- state = :value_normal
- value << char
- end
- when :value_normal_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_normal
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Invalid escaped hex code \"\\#{hex_buffer}#{char}\"")
- end
- when :value_quoted then
- case char
- when '\\' then state = :value_quoted_escape
- when '"' then state = :value_end
- else value << char
- end
- when :value_quoted_escape then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted_escape_hex
- hex_buffer = char
- else
- state = :value_quoted
- value << char
- end
- when :value_quoted_escape_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_quoted
- value << "#{hex_buffer}#{char}".to_i(16).chr
- else raise(MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"#{char}\"")
- end
- when :value_hexstring then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring_hex
- value << char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the first character of a hex pair, but got \"#{char}\"")
- end
- when :value_hexstring_hex then
- case char
- when '0'..'9', 'a'..'f', 'A'..'F' then
- state = :value_hexstring
- value << char
- else raise(MalformedError, "Expected the second character of a hex pair, but got \"#{char}\"")
- end
- when :value_end then
- case char
- when ' ' then state = :value_end
- when ',' then
- state = :key
- yield key.string.strip, rstrip_except_escaped(value.string, dn_index)
- key = StringIO.new
- value = StringIO.new
- else raise(MalformedError, "Expected the end of an attribute value, but got \"#{char}\"")
- end
- else raise "Fell out of state machine"
- end
- end
-
- # Last pair
- raise(MalformedError, 'DN string ended unexpectedly') unless
- [:value, :value_normal, :value_hexstring, :value_end].include? state
-
- yield key.string.strip, rstrip_except_escaped(value.string, @dn.length)
- end
-
- def rstrip_except_escaped(str, dn_index)
- str_ends_with_whitespace = str.match(/\s\z/)
-
- if str_ends_with_whitespace
- dn_part_ends_with_escaped_whitespace = @dn[0, dn_index].match(/\\(\s+)\z/)
-
- if dn_part_ends_with_escaped_whitespace
- dn_part_rwhitespace = dn_part_ends_with_escaped_whitespace[1]
- num_chars_to_remove = dn_part_rwhitespace.length - 1
- str = str[0, str.length - num_chars_to_remove]
- else
- str.rstrip!
- end
- end
-
- str
- end
-
- ##
- # Returns the DN as an array in the form expected by the constructor.
- def to_a
- a = []
- self.each_pair { |key, value| a << key << value } unless @dn.empty?
- a
- end
-
- ##
- # Return the DN as an escaped string.
- def to_s
- @dn
- end
-
- ##
- # Return the DN as an escaped and normalized string.
- def to_normalized_s
- self.class.new(*to_a).to_s.downcase
- end
-
- # https://tools.ietf.org/html/rfc4514 section 2.4 lists these exceptions
- # for DN values. All of the following must be escaped in any normal string
- # using a single backslash ('\') as escape. The space character is left
- # out here because in a "normalized" string, spaces should only be escaped
- # if necessary (i.e. leading or trailing space).
- NORMAL_ESCAPES = [',', '+', '"', '\\', '<', '>', ';', '='].freeze
-
- # The following must be represented as escaped hex
- HEX_ESCAPES = {
- "\n" => '\0a',
- "\r" => '\0d'
- }.freeze
-
- # Compiled character class regexp using the keys from the above hash, and
- # checking for a space or # at the start, or space at the end, of the
- # string.
- ESCAPE_RE = Regexp.new("(^ |^#| $|[" +
- NORMAL_ESCAPES.map { |e| Regexp.escape(e) }.join +
- "])")
-
- HEX_ESCAPE_RE = Regexp.new("([" +
- HEX_ESCAPES.keys.map { |e| Regexp.escape(e) }.join +
- "])")
-
- ##
- # Escape a string for use in a DN value
- def self.escape(string)
- escaped = string.gsub(ESCAPE_RE) { |char| "\\" + char }
- escaped.gsub(HEX_ESCAPE_RE) { |char| HEX_ESCAPES[char] }
- end
-
- private
-
- def initialize_array(args)
- buffer = StringIO.new
-
- args.each_with_index do |arg, index|
- if index.even? # key
- buffer << "," if index > 0
- buffer << arg
- else # value
- buffer << "="
- buffer << self.class.escape(arg)
- end
- end
-
- @dn = buffer.string
- end
-
- def initialize_string(arg)
- @dn = arg.to_s
- end
-
- ##
- # Proxy all other requests to the string object, because a DN is mainly
- # used within the library as a string
- # rubocop:disable GitlabSecurity/PublicSend
- def method_missing(method, *args, &block)
- @dn.send(method, *args, &block)
- end
-
- ##
- # Redefined to be consistent with redefined `method_missing` behavior
- def respond_to?(sym, include_private = false)
- @dn.respond_to?(sym, include_private)
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/person.rb b/lib/gitlab/ldap/person.rb
deleted file mode 100644
index c59df556247..00000000000
--- a/lib/gitlab/ldap/person.rb
+++ /dev/null
@@ -1,120 +0,0 @@
-module Gitlab
- module LDAP
- class Person
- # Active Directory-specific LDAP filter that checks if bit 2 of the
- # userAccountControl attribute is set.
- # Source: http://ctogonewild.com/2009/09/03/bitmask-searches-in-ldap/
- AD_USER_DISABLED = Net::LDAP::Filter.ex("userAccountControl:1.2.840.113556.1.4.803", "2")
-
- InvalidEntryError = Class.new(StandardError)
-
- attr_accessor :entry, :provider
-
- def self.find_by_uid(uid, adapter)
- uid = Net::LDAP::Filter.escape(uid)
- adapter.user(adapter.config.uid, uid)
- end
-
- def self.find_by_dn(dn, adapter)
- adapter.user('dn', dn)
- end
-
- def self.find_by_email(email, adapter)
- email_fields = adapter.config.attributes['email']
-
- adapter.user(email_fields, email)
- end
-
- def self.disabled_via_active_directory?(dn, adapter)
- adapter.dn_matches_filter?(dn, AD_USER_DISABLED)
- end
-
- def self.ldap_attributes(config)
- [
- 'dn',
- config.uid,
- *config.attributes['name'],
- *config.attributes['email'],
- *config.attributes['username']
- ].compact.uniq
- end
-
- def self.normalize_dn(dn)
- ::Gitlab::LDAP::DN.new(dn).to_normalized_s
- rescue ::Gitlab::LDAP::DN::FormatError => e
- Rails.logger.info("Returning original DN \"#{dn}\" due to error during normalization attempt: #{e.message}")
-
- dn
- end
-
- # Returns the UID in a normalized form.
- #
- # 1. Excess spaces are stripped
- # 2. The string is downcased (for case-insensitivity)
- def self.normalize_uid(uid)
- ::Gitlab::LDAP::DN.normalize_value(uid)
- rescue ::Gitlab::LDAP::DN::FormatError => e
- Rails.logger.info("Returning original UID \"#{uid}\" due to error during normalization attempt: #{e.message}")
-
- uid
- end
-
- def initialize(entry, provider)
- Rails.logger.debug { "Instantiating #{self.class.name} with LDIF:\n#{entry.to_ldif}" }
- @entry = entry
- @provider = provider
- end
-
- def name
- attribute_value(:name).first
- end
-
- def uid
- entry.public_send(config.uid).first # rubocop:disable GitlabSecurity/PublicSend
- end
-
- def username
- username = attribute_value(:username)
-
- # Depending on the attribute, multiple values may
- # be returned. We need only one for username.
- # Ex. `uid` returns only one value but `mail` may
- # return an array of multiple email addresses.
- [username].flatten.first.tap do |username|
- username.downcase! if config.lowercase_usernames
- end
- end
-
- def email
- attribute_value(:email)
- end
-
- def dn
- self.class.normalize_dn(entry.dn)
- end
-
- private
-
- def entry
- @entry
- end
-
- def config
- @config ||= Gitlab::LDAP::Config.new(provider)
- end
-
- # Using the LDAP attributes configuration, find and return the first
- # attribute with a value. For example, by default, when given 'email',
- # this method looks for 'mail', 'email' and 'userPrincipalName' and
- # returns the first with a value.
- def attribute_value(attribute)
- attributes = Array(config.attributes[attribute.to_s])
- selected_attr = attributes.find { |attr| entry.respond_to?(attr) }
-
- return nil unless selected_attr
-
- entry.public_send(selected_attr) # rubocop:disable GitlabSecurity/PublicSend
- end
- end
- end
-end
diff --git a/lib/gitlab/ldap/user.rb b/lib/gitlab/ldap/user.rb
deleted file mode 100644
index 84ee94e38e4..00000000000
--- a/lib/gitlab/ldap/user.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-# LDAP extension for User model
-#
-# * Find or create user from omniauth.auth data
-# * Links LDAP account with existing user
-# * Auth LDAP user with login and password
-#
-module Gitlab
- module LDAP
- class User < Gitlab::OAuth::User
- class << self
- def find_by_uid_and_provider(uid, provider)
- identity = ::Identity.with_extern_uid(provider, uid).take
-
- identity && identity.user
- end
- end
-
- def save
- super('LDAP')
- end
-
- # instance methods
- def find_user
- find_by_uid_and_provider || find_by_email || build_new_user
- end
-
- def find_by_uid_and_provider
- self.class.find_by_uid_and_provider(auth_hash.uid, auth_hash.provider)
- end
-
- def changed?
- gl_user.changed? || gl_user.identities.any?(&:changed?)
- end
-
- def block_after_signup?
- ldap_config.block_auto_created_users
- end
-
- def allowed?
- Gitlab::LDAP::Access.allowed?(gl_user)
- end
-
- def ldap_config
- Gitlab::LDAP::Config.new(auth_hash.provider)
- end
-
- def auth_hash=(auth_hash)
- @auth_hash = Gitlab::LDAP::AuthHash.new(auth_hash)
- end
- end
- end
-end
diff --git a/lib/gitlab/middleware/read_only.rb b/lib/gitlab/middleware/read_only.rb
index c26656704d7..d9d5f90596f 100644
--- a/lib/gitlab/middleware/read_only.rb
+++ b/lib/gitlab/middleware/read_only.rb
@@ -1,90 +1,19 @@
module Gitlab
module Middleware
class ReadOnly
- DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
- APPLICATION_JSON = 'application/json'.freeze
API_VERSIONS = (3..4)
+ def self.internal_routes
+ @internal_routes ||=
+ API_VERSIONS.map { |version| "api/v#{version}/internal" }
+ end
+
def initialize(app)
@app = app
- @whitelisted = internal_routes
end
def call(env)
- @env = env
- @route_hash = nil
-
- if disallowed_request? && Gitlab::Database.read_only?
- Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
- error_message = 'You cannot do writing operations on a read-only GitLab instance'
-
- if json_request?
- return [403, { 'Content-Type' => 'application/json' }, [{ 'message' => error_message }.to_json]]
- else
- rack_flash.alert = error_message
- rack_session['flash'] = rack_flash.to_session_value
-
- return [301, { 'Location' => last_visited_url }, []]
- end
- end
-
- @app.call(env)
- end
-
- private
-
- def internal_routes
- API_VERSIONS.flat_map { |version| "api/v#{version}/internal" }
- end
-
- def disallowed_request?
- DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) && !whitelisted_routes
- end
-
- def json_request?
- request.media_type == APPLICATION_JSON
- end
-
- def rack_flash
- @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
- end
-
- def rack_session
- @env['rack.session']
- end
-
- def request
- @env['rack.request'] ||= Rack::Request.new(@env)
- end
-
- def last_visited_url
- @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url
- end
-
- def route_hash
- @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
- end
-
- def whitelisted_routes
- grack_route || @whitelisted.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
- end
-
- def sidekiq_route
- request.path.start_with?('/admin/sidekiq')
- end
-
- def grack_route
- # Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.path.end_with?('.git/git-upload-pack')
-
- route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
- end
-
- def lfs_route
- # Calling route_hash may be expensive. Only do it if we think there's a possible match
- return false unless request.path.end_with?('/info/lfs/objects/batch')
-
- route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
+ ReadOnly::Controller.new(@app, env).call
end
end
end
diff --git a/lib/gitlab/middleware/read_only/controller.rb b/lib/gitlab/middleware/read_only/controller.rb
new file mode 100644
index 00000000000..45b644e6510
--- /dev/null
+++ b/lib/gitlab/middleware/read_only/controller.rb
@@ -0,0 +1,86 @@
+module Gitlab
+ module Middleware
+ class ReadOnly
+ class Controller
+ DISALLOWED_METHODS = %w(POST PATCH PUT DELETE).freeze
+ APPLICATION_JSON = 'application/json'.freeze
+ ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'.freeze
+
+ def initialize(app, env)
+ @app = app
+ @env = env
+ end
+
+ def call
+ if disallowed_request? && Gitlab::Database.read_only?
+ Rails.logger.debug('GitLab ReadOnly: preventing possible non read-only operation')
+
+ if json_request?
+ return [403, { 'Content-Type' => APPLICATION_JSON }, [{ 'message' => ERROR_MESSAGE }.to_json]]
+ else
+ rack_flash.alert = ERROR_MESSAGE
+ rack_session['flash'] = rack_flash.to_session_value
+
+ return [301, { 'Location' => last_visited_url }, []]
+ end
+ end
+
+ @app.call(@env)
+ end
+
+ private
+
+ def disallowed_request?
+ DISALLOWED_METHODS.include?(@env['REQUEST_METHOD']) &&
+ !whitelisted_routes
+ end
+
+ def json_request?
+ request.media_type == APPLICATION_JSON
+ end
+
+ def rack_flash
+ @rack_flash ||= ActionDispatch::Flash::FlashHash.from_session_value(rack_session)
+ end
+
+ def rack_session
+ @env['rack.session']
+ end
+
+ def request
+ @env['rack.request'] ||= Rack::Request.new(@env)
+ end
+
+ def last_visited_url
+ @env['HTTP_REFERER'] || rack_session['user_return_to'] || Gitlab::Routing.url_helpers.root_url
+ end
+
+ def route_hash
+ @route_hash ||= Rails.application.routes.recognize_path(request.url, { method: request.request_method }) rescue {}
+ end
+
+ def whitelisted_routes
+ grack_route || ReadOnly.internal_routes.any? { |path| request.path.include?(path) } || lfs_route || sidekiq_route
+ end
+
+ def sidekiq_route
+ request.path.start_with?('/admin/sidekiq')
+ end
+
+ def grack_route
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('.git/git-upload-pack')
+
+ route_hash[:controller] == 'projects/git_http' && route_hash[:action] == 'git_upload_pack'
+ end
+
+ def lfs_route
+ # Calling route_hash may be expensive. Only do it if we think there's a possible match
+ return false unless request.path.end_with?('/info/lfs/objects/batch')
+
+ route_hash[:controller] == 'projects/lfs_api' && route_hash[:action] == 'batch'
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/middleware/release_env.rb b/lib/gitlab/middleware/release_env.rb
new file mode 100644
index 00000000000..f8d0a135965
--- /dev/null
+++ b/lib/gitlab/middleware/release_env.rb
@@ -0,0 +1,14 @@
+module Gitlab
+ module Middleware
+ # Some of middleware would hold env for no good reason even after the
+ # request had already been processed, and we could not garbage collect
+ # them due to this. Put this middleware as the first middleware so that
+ # it would clear the env after the request is done, allowing GC gets a
+ # chance to release memory for the last request.
+ ReleaseEnv = Struct.new(:app) do
+ def call(env)
+ app.call(env).tap { env.clear }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/o_auth.rb b/lib/gitlab/o_auth.rb
deleted file mode 100644
index 5ad8d83bd6e..00000000000
--- a/lib/gitlab/o_auth.rb
+++ /dev/null
@@ -1,6 +0,0 @@
-module Gitlab
- module OAuth
- SignupDisabledError = Class.new(StandardError)
- SigninDisabledForProviderError = Class.new(StandardError)
- end
-end
diff --git a/lib/gitlab/o_auth/auth_hash.rb b/lib/gitlab/o_auth/auth_hash.rb
deleted file mode 100644
index 5b5ed449f94..00000000000
--- a/lib/gitlab/o_auth/auth_hash.rb
+++ /dev/null
@@ -1,90 +0,0 @@
-# Class to parse and transform the info provided by omniauth
-#
-module Gitlab
- module OAuth
- class AuthHash
- attr_reader :auth_hash
- def initialize(auth_hash)
- @auth_hash = auth_hash
- end
-
- def uid
- @uid ||= Gitlab::Utils.force_utf8(auth_hash.uid.to_s)
- end
-
- def provider
- @provider ||= auth_hash.provider.to_s
- end
-
- def name
- @name ||= get_info(:name) || "#{get_info(:first_name)} #{get_info(:last_name)}"
- end
-
- def username
- @username ||= username_and_email[:username].to_s
- end
-
- def email
- @email ||= username_and_email[:email].to_s
- end
-
- def password
- @password ||= Gitlab::Utils.force_utf8(Devise.friendly_token[0, 8].downcase)
- end
-
- def location
- location = get_info(:address)
- if location.is_a?(Hash)
- [location.locality.presence, location.country.presence].compact.join(', ')
- else
- location
- end
- end
-
- def has_attribute?(attribute)
- if attribute == :location
- get_info(:address).present?
- else
- get_info(attribute).present?
- end
- end
-
- private
-
- def info
- auth_hash.info
- end
-
- def get_info(key)
- value = info[key]
- Gitlab::Utils.force_utf8(value) if value
- value
- end
-
- def username_and_email
- @username_and_email ||= begin
- username = get_info(:username).presence || get_info(:nickname).presence
- email = get_info(:email).presence
-
- username ||= generate_username(email) if email
- email ||= generate_temporarily_email(username) if username
-
- {
- username: username,
- email: email
- }
- end
- end
-
- # Get the first part of the email address (before @)
- # In addtion in removes illegal characters
- def generate_username(email)
- email.match(/^[^@]*/)[0].mb_chars.normalize(:kd).gsub(/[^\x00-\x7F]/, '').to_s
- end
-
- def generate_temporarily_email(username)
- "temp-email-for-oauth-#{username}@gitlab.localhost"
- end
- end
- end
-end
diff --git a/lib/gitlab/o_auth/provider.rb b/lib/gitlab/o_auth/provider.rb
deleted file mode 100644
index 657db29c85a..00000000000
--- a/lib/gitlab/o_auth/provider.rb
+++ /dev/null
@@ -1,54 +0,0 @@
-module Gitlab
- module OAuth
- class Provider
- LABELS = {
- "github" => "GitHub",
- "gitlab" => "GitLab.com",
- "google_oauth2" => "Google"
- }.freeze
-
- def self.providers
- Devise.omniauth_providers
- end
-
- def self.enabled?(name)
- providers.include?(name.to_sym)
- end
-
- def self.ldap_provider?(name)
- name.to_s.start_with?('ldap')
- end
-
- def self.sync_profile_from_provider?(provider)
- return true if ldap_provider?(provider)
-
- providers = Gitlab.config.omniauth.sync_profile_from_provider
-
- if providers.is_a?(Array)
- providers.include?(provider)
- else
- providers
- end
- end
-
- def self.config_for(name)
- name = name.to_s
- if ldap_provider?(name)
- if Gitlab::LDAP::Config.valid_provider?(name)
- Gitlab::LDAP::Config.new(name).options
- else
- nil
- end
- else
- Gitlab.config.omniauth.providers.find { |provider| provider.name == name }
- end
- end
-
- def self.label_for(name)
- name = name.to_s
- config = config_for(name)
- (config && config['label']) || LABELS[name] || name.titleize
- end
- end
- end
-end
diff --git a/lib/gitlab/o_auth/session.rb b/lib/gitlab/o_auth/session.rb
deleted file mode 100644
index 30739f2a2c5..00000000000
--- a/lib/gitlab/o_auth/session.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# :nocov:
-module Gitlab
- module OAuth
- module Session
- def self.create(provider, ticket)
- Rails.cache.write("gitlab:#{provider}:#{ticket}", ticket, expires_in: Gitlab.config.omniauth.cas3.session_duration)
- end
-
- def self.destroy(provider, ticket)
- Rails.cache.delete("gitlab:#{provider}:#{ticket}")
- end
-
- def self.valid?(provider, ticket)
- Rails.cache.read("gitlab:#{provider}:#{ticket}").present?
- end
- end
- end
-end
-# :nocov:
diff --git a/lib/gitlab/o_auth/user.rb b/lib/gitlab/o_auth/user.rb
deleted file mode 100644
index 28ebac1776e..00000000000
--- a/lib/gitlab/o_auth/user.rb
+++ /dev/null
@@ -1,241 +0,0 @@
-# OAuth extension for User model
-#
-# * Find GitLab user based on omniauth uid and provider
-# * Create new user from omniauth data
-#
-module Gitlab
- module OAuth
- class User
- attr_accessor :auth_hash, :gl_user
-
- def initialize(auth_hash)
- self.auth_hash = auth_hash
- update_profile
- add_or_update_user_identities
- end
-
- def persisted?
- gl_user.try(:persisted?)
- end
-
- def new?
- !persisted?
- end
-
- def valid?
- gl_user.try(:valid?)
- end
-
- def save(provider = 'OAuth')
- raise SigninDisabledForProviderError if oauth_provider_disabled?
- raise SignupDisabledError unless gl_user
-
- block_after_save = needs_blocking?
-
- Users::UpdateService.new(gl_user, user: gl_user).execute!
-
- gl_user.block if block_after_save
-
- log.info "(#{provider}) saving user #{auth_hash.email} from login with extern_uid => #{auth_hash.uid}"
- gl_user
- rescue ActiveRecord::RecordInvalid => e
- log.info "(#{provider}) Error saving user #{auth_hash.uid} (#{auth_hash.email}): #{gl_user.errors.full_messages}"
- return self, e.record.errors
- end
-
- def gl_user
- return @gl_user if defined?(@gl_user)
-
- @gl_user = find_user
- end
-
- def find_user
- user = find_by_uid_and_provider
-
- user ||= find_or_build_ldap_user if auto_link_ldap_user?
- user ||= build_new_user if signup_enabled?
-
- user.external = true if external_provider? && user&.new_record?
-
- user
- end
-
- protected
-
- def add_or_update_user_identities
- return unless gl_user
-
- # find_or_initialize_by doesn't update `gl_user.identities`, and isn't autosaved.
- identity = gl_user.identities.find { |identity| identity.provider == auth_hash.provider }
-
- identity ||= gl_user.identities.build(provider: auth_hash.provider)
- identity.extern_uid = auth_hash.uid
-
- if auto_link_ldap_user? && !gl_user.ldap_user? && ldap_person
- log.info "Correct LDAP account has been found. identity to user: #{gl_user.username}."
- gl_user.identities.build(provider: ldap_person.provider, extern_uid: ldap_person.dn)
- end
- end
-
- def find_or_build_ldap_user
- return unless ldap_person
-
- user = Gitlab::LDAP::User.find_by_uid_and_provider(ldap_person.dn, ldap_person.provider)
- if user
- log.info "LDAP account found for user #{user.username}. Building new #{auth_hash.provider} identity."
- return user
- end
-
- log.info "No user found using #{auth_hash.provider} provider. Creating a new one."
- build_new_user
- end
-
- def find_by_email
- return unless auth_hash.has_attribute?(:email)
-
- ::User.find_by(email: auth_hash.email.downcase)
- end
-
- def auto_link_ldap_user?
- Gitlab.config.omniauth.auto_link_ldap_user
- end
-
- def creating_linked_ldap_user?
- auto_link_ldap_user? && ldap_person
- end
-
- def ldap_person
- return @ldap_person if defined?(@ldap_person)
-
- # Look for a corresponding person with same uid in any of the configured LDAP providers
- Gitlab::LDAP::Config.providers.each do |provider|
- adapter = Gitlab::LDAP::Adapter.new(provider)
- @ldap_person = find_ldap_person(auth_hash, adapter)
- break if @ldap_person
- end
- @ldap_person
- end
-
- def find_ldap_person(auth_hash, adapter)
- Gitlab::LDAP::Person.find_by_uid(auth_hash.uid, adapter) ||
- Gitlab::LDAP::Person.find_by_email(auth_hash.uid, adapter) ||
- Gitlab::LDAP::Person.find_by_dn(auth_hash.uid, adapter)
- end
-
- def ldap_config
- Gitlab::LDAP::Config.new(ldap_person.provider) if ldap_person
- end
-
- def needs_blocking?
- new? && block_after_signup?
- end
-
- def signup_enabled?
- providers = Gitlab.config.omniauth.allow_single_sign_on
- if providers.is_a?(Array)
- providers.include?(auth_hash.provider)
- else
- providers
- end
- end
-
- def external_provider?
- Gitlab.config.omniauth.external_providers.include?(auth_hash.provider)
- end
-
- def block_after_signup?
- if creating_linked_ldap_user?
- ldap_config.block_auto_created_users
- else
- Gitlab.config.omniauth.block_auto_created_users
- end
- end
-
- def auth_hash=(auth_hash)
- @auth_hash = AuthHash.new(auth_hash)
- end
-
- def find_by_uid_and_provider
- identity = Identity.with_extern_uid(auth_hash.provider, auth_hash.uid).take
- identity && identity.user
- end
-
- def build_new_user
- user_params = user_attributes.merge(skip_confirmation: true)
- Users::BuildService.new(nil, user_params).execute(skip_authorization: true)
- end
-
- def user_attributes
- # Give preference to LDAP for sensitive information when creating a linked account
- if creating_linked_ldap_user?
- username = ldap_person.username.presence
- email = ldap_person.email.first.presence
- end
-
- username ||= auth_hash.username
- email ||= auth_hash.email
-
- valid_username = ::Namespace.clean_path(username)
-
- uniquify = Uniquify.new
- valid_username = uniquify.string(valid_username) { |s| !NamespacePathValidator.valid_path?(s) }
-
- name = auth_hash.name
- name = valid_username if name.strip.empty?
-
- {
- name: name,
- username: valid_username,
- email: email,
- password: auth_hash.password,
- password_confirmation: auth_hash.password,
- password_automatically_set: true
- }
- end
-
- def sync_profile_from_provider?
- Gitlab::OAuth::Provider.sync_profile_from_provider?(auth_hash.provider)
- end
-
- def update_profile
- clear_user_synced_attributes_metadata
-
- return unless sync_profile_from_provider? || creating_linked_ldap_user?
-
- metadata = gl_user.build_user_synced_attributes_metadata
-
- if sync_profile_from_provider?
- UserSyncedAttributesMetadata::SYNCABLE_ATTRIBUTES.each do |key|
- if auth_hash.has_attribute?(key) && gl_user.sync_attribute?(key)
- gl_user[key] = auth_hash.public_send(key) # rubocop:disable GitlabSecurity/PublicSend
- metadata.set_attribute_synced(key, true)
- else
- metadata.set_attribute_synced(key, false)
- end
- end
-
- metadata.provider = auth_hash.provider
- end
-
- if creating_linked_ldap_user? && gl_user.email == ldap_person.email.first
- metadata.set_attribute_synced(:email, true)
- metadata.provider = ldap_person.provider
- end
- end
-
- def clear_user_synced_attributes_metadata
- gl_user&.user_synced_attributes_metadata&.destroy
- end
-
- def log
- Gitlab::AppLogger
- end
-
- def oauth_provider_disabled?
- Gitlab::CurrentSettings.current_application_settings
- .disabled_oauth_sign_in_sources
- .include?(auth_hash.provider)
- end
- end
- end
-end
diff --git a/lib/gitlab/plugin.rb b/lib/gitlab/plugin.rb
new file mode 100644
index 00000000000..0d1cb16b378
--- /dev/null
+++ b/lib/gitlab/plugin.rb
@@ -0,0 +1,26 @@
+module Gitlab
+ module Plugin
+ def self.files
+ Dir.glob(Rails.root.join('plugins/*')).select do |entry|
+ File.file?(entry)
+ end
+ end
+
+ def self.execute_all_async(data)
+ args = files.map { |file| [file, data] }
+
+ PluginWorker.bulk_perform_async(args)
+ end
+
+ def self.execute(file, data)
+ result = Gitlab::Popen.popen_with_detail([file]) do |stdin|
+ stdin.write(data.to_json)
+ end
+
+ exit_status = result.status&.exitstatus
+ [exit_status.zero?, result.stderr]
+ rescue => e
+ [false, e.message]
+ end
+ end
+end
diff --git a/lib/gitlab/plugin_logger.rb b/lib/gitlab/plugin_logger.rb
new file mode 100644
index 00000000000..c4f6ec3e21d
--- /dev/null
+++ b/lib/gitlab/plugin_logger.rb
@@ -0,0 +1,7 @@
+module Gitlab
+ class PluginLogger < Gitlab::Logger
+ def self.file_name_noext
+ 'plugin'
+ end
+ end
+end
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index cf0935dbd9a..29277ec6481 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -29,8 +29,18 @@ module Gitlab
@blobs_count ||= blobs.count
end
- def notes_count
- @notes_count ||= notes.count
+ def limited_notes_count
+ return @limited_notes_count if defined?(@limited_notes_count)
+
+ types = %w(issue merge_request commit snippet)
+ @limited_notes_count = 0
+
+ types.each do |type|
+ @limited_notes_count += notes_finder(type).limit(count_limit).count
+ break if @limited_notes_count >= count_limit
+ end
+
+ @limited_notes_count
end
def wiki_blobs_count
@@ -72,11 +82,12 @@ module Gitlab
end
def single_commit_result?
- commits_count == 1 && total_result_count == 1
- end
+ return false if commits_count != 1
- def total_result_count
- issues_count + merge_requests_count + milestones_count + notes_count + blobs_count + wiki_blobs_count + commits_count
+ counts = %i(limited_milestones_count limited_notes_count
+ limited_merge_requests_count limited_issues_count
+ blobs_count wiki_blobs_count)
+ counts.all? { |count_method| public_send(count_method).zero? } # rubocop:disable GitlabSecurity/PublicSend
end
private
@@ -106,7 +117,11 @@ module Gitlab
end
def notes
- @notes ||= NotesFinder.new(project, @current_user, search: query).execute.user.order('updated_at DESC')
+ @notes ||= notes_finder(nil)
+ end
+
+ def notes_finder(type)
+ NotesFinder.new(project, @current_user, search: query, target_type: type).execute.user.order('updated_at DESC')
end
def commits
diff --git a/lib/gitlab/saml/auth_hash.rb b/lib/gitlab/saml/auth_hash.rb
deleted file mode 100644
index 33d19373098..00000000000
--- a/lib/gitlab/saml/auth_hash.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module Gitlab
- module Saml
- class AuthHash < Gitlab::OAuth::AuthHash
- def groups
- Array.wrap(get_raw(Gitlab::Saml::Config.groups))
- end
-
- private
-
- def get_raw(key)
- # Needs to call `all` because of https://git.io/vVo4u
- # otherwise just the first value is returned
- auth_hash.extra[:raw_info].all[key]
- end
- end
- end
-end
diff --git a/lib/gitlab/saml/config.rb b/lib/gitlab/saml/config.rb
deleted file mode 100644
index 574c3a4b28c..00000000000
--- a/lib/gitlab/saml/config.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-module Gitlab
- module Saml
- class Config
- class << self
- def options
- Gitlab.config.omniauth.providers.find { |provider| provider.name == 'saml' }
- end
-
- def groups
- options[:groups_attribute]
- end
-
- def external_groups
- options[:external_groups]
- end
- end
- end
- end
-end
diff --git a/lib/gitlab/saml/user.rb b/lib/gitlab/saml/user.rb
deleted file mode 100644
index d8faf7aad8c..00000000000
--- a/lib/gitlab/saml/user.rb
+++ /dev/null
@@ -1,50 +0,0 @@
-# SAML extension for User model
-#
-# * Find GitLab user based on SAML uid and provider
-# * Create new user from SAML data
-#
-module Gitlab
- module Saml
- class User < Gitlab::OAuth::User
- def save
- super('SAML')
- end
-
- def find_user
- user = find_by_uid_and_provider
-
- user ||= find_by_email if auto_link_saml_user?
- user ||= find_or_build_ldap_user if auto_link_ldap_user?
- user ||= build_new_user if signup_enabled?
-
- if external_users_enabled? && user
- # Check if there is overlap between the user's groups and the external groups
- # setting then set user as external or internal.
- user.external = !(auth_hash.groups & Gitlab::Saml::Config.external_groups).empty?
- end
-
- user
- end
-
- def changed?
- return true unless gl_user
-
- gl_user.changed? || gl_user.identities.any?(&:changed?)
- end
-
- protected
-
- def auto_link_saml_user?
- Gitlab.config.omniauth.auto_link_saml_user
- end
-
- def external_users_enabled?
- !Gitlab::Saml::Config.external_groups.nil?
- end
-
- def auth_hash=(auth_hash)
- @auth_hash = Gitlab::Saml::AuthHash.new(auth_hash)
- end
- end
- end
-end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index 5a5ae7f19d4..757ef71b95a 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -1,6 +1,8 @@
module Gitlab
class SearchResults
class FoundBlob
+ include EncodingHelper
+
attr_reader :id, :filename, :basename, :ref, :startline, :data, :project_id
def initialize(opts = {})
@@ -9,7 +11,7 @@ module Gitlab
@basename = opts.fetch(:basename, nil)
@ref = opts.fetch(:ref, nil)
@startline = opts.fetch(:startline, nil)
- @data = opts.fetch(:data, nil)
+ @data = encode_utf8(opts.fetch(:data, nil))
@per_page = opts.fetch(:per_page, 20)
@project_id = opts.fetch(:project_id, nil)
end
@@ -60,22 +62,6 @@ module Gitlab
without_count ? collection.without_count : collection
end
- def projects_count
- @projects_count ||= projects.count
- end
-
- def issues_count
- @issues_count ||= issues.count
- end
-
- def merge_requests_count
- @merge_requests_count ||= merge_requests.count
- end
-
- def milestones_count
- @milestones_count ||= milestones.count
- end
-
def limited_projects_count
@limited_projects_count ||= projects.limit(count_limit).count
end
diff --git a/lib/gitlab/sidekiq_middleware/memory_killer.rb b/lib/gitlab/sidekiq_middleware/memory_killer.rb
deleted file mode 100644
index b89ae2505c9..00000000000
--- a/lib/gitlab/sidekiq_middleware/memory_killer.rb
+++ /dev/null
@@ -1,67 +0,0 @@
-module Gitlab
- module SidekiqMiddleware
- class MemoryKiller
- # Default the RSS limit to 0, meaning the MemoryKiller is disabled
- MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i
- # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit
- GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
- # Wait 30 seconds for running jobs to finish during graceful shutdown
- SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
-
- # Create a mutex used to ensure there will be only one thread waiting to
- # shut Sidekiq down
- MUTEX = Mutex.new
-
- def call(worker, job, queue)
- yield
-
- current_rss = get_rss
-
- return unless MAX_RSS > 0 && current_rss > MAX_RSS
-
- Thread.new do
- # Return if another thread is already waiting to shut Sidekiq down
- return unless MUTEX.try_lock
-
- Sidekiq.logger.warn "Sidekiq worker PID-#{pid} current RSS #{current_rss}"\
- " exceeds maximum RSS #{MAX_RSS} after finishing job #{worker.class} JID-#{job['jid']}"
- Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
-
- # Wait `GRACE_TIME` to give the memory intensive job time to finish.
- # Then, tell Sidekiq to stop fetching new jobs.
- wait_and_signal(GRACE_TIME, 'SIGSTP', 'stop fetching new jobs')
-
- # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
- # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
- # moments to finish, killing and requeuing them if they didn't, and
- # then terminating itself.
- wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
-
- # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
- wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
- end
- end
-
- private
-
- def get_rss
- output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s)
- return 0 unless status.zero?
-
- output.to_i
- end
-
- def wait_and_signal(time, signal, explanation)
- Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
- sleep(time)
-
- Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
- Process.kill(signal, pid)
- end
-
- def pid
- Process.pid
- end
- end
- end
-end
diff --git a/lib/gitlab/sidekiq_middleware/shutdown.rb b/lib/gitlab/sidekiq_middleware/shutdown.rb
new file mode 100644
index 00000000000..c2b8d6de66e
--- /dev/null
+++ b/lib/gitlab/sidekiq_middleware/shutdown.rb
@@ -0,0 +1,133 @@
+require 'mutex_m'
+
+module Gitlab
+ module SidekiqMiddleware
+ class Shutdown
+ extend Mutex_m
+
+ # Default the RSS limit to 0, meaning the MemoryKiller is disabled
+ MAX_RSS = (ENV['SIDEKIQ_MEMORY_KILLER_MAX_RSS'] || 0).to_s.to_i
+ # Give Sidekiq 15 minutes of grace time after exceeding the RSS limit
+ GRACE_TIME = (ENV['SIDEKIQ_MEMORY_KILLER_GRACE_TIME'] || 15 * 60).to_s.to_i
+ # Wait 30 seconds for running jobs to finish during graceful shutdown
+ SHUTDOWN_WAIT = (ENV['SIDEKIQ_MEMORY_KILLER_SHUTDOWN_WAIT'] || 30).to_s.to_i
+
+ # This exception can be used to request that the middleware start shutting down Sidekiq
+ WantShutdown = Class.new(StandardError)
+
+ ShutdownWithoutRaise = Class.new(WantShutdown)
+ private_constant :ShutdownWithoutRaise
+
+ # For testing only, to avoid race conditions (?) in Rspec mocks.
+ attr_reader :trace
+
+ # We store the shutdown thread in a class variable to ensure that there
+ # can be only one shutdown thread in the process.
+ def self.create_shutdown_thread
+ mu_synchronize do
+ return unless @shutdown_thread.nil?
+
+ @shutdown_thread = Thread.new { yield }
+ end
+ end
+
+ # For testing only: so we can wait for the shutdown thread to finish.
+ def self.shutdown_thread
+ mu_synchronize { @shutdown_thread }
+ end
+
+ # For testing only: so that we can reset the global state before each test.
+ def self.clear_shutdown_thread
+ mu_synchronize { @shutdown_thread = nil }
+ end
+
+ def initialize
+ @trace = Queue.new if Rails.env.test?
+ end
+
+ def call(worker, job, queue)
+ shutdown_exception = nil
+
+ begin
+ yield
+ check_rss!
+ rescue WantShutdown => ex
+ shutdown_exception = ex
+ end
+
+ return unless shutdown_exception
+
+ self.class.create_shutdown_thread do
+ do_shutdown(worker, job, shutdown_exception)
+ end
+
+ raise shutdown_exception unless shutdown_exception.is_a?(ShutdownWithoutRaise)
+ end
+
+ private
+
+ def do_shutdown(worker, job, shutdown_exception)
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} shutting down because of #{shutdown_exception} after job "\
+ "#{worker.class} JID-#{job['jid']}"
+ Sidekiq.logger.warn "Sidekiq worker PID-#{pid} will stop fetching new jobs in #{GRACE_TIME} seconds, and will be shut down #{SHUTDOWN_WAIT} seconds later"
+
+ # Wait `GRACE_TIME` to give the memory intensive job time to finish.
+ # Then, tell Sidekiq to stop fetching new jobs.
+ wait_and_signal(GRACE_TIME, 'SIGTSTP', 'stop fetching new jobs')
+
+ # Wait `SHUTDOWN_WAIT` to give already fetched jobs time to finish.
+ # Then, tell Sidekiq to gracefully shut down by giving jobs a few more
+ # moments to finish, killing and requeuing them if they didn't, and
+ # then terminating itself.
+ wait_and_signal(SHUTDOWN_WAIT, 'SIGTERM', 'gracefully shut down')
+
+ # Wait for Sidekiq to shutdown gracefully, and kill it if it didn't.
+ wait_and_signal(Sidekiq.options[:timeout] + 2, 'SIGKILL', 'die')
+ end
+
+ def check_rss!
+ return unless MAX_RSS > 0
+
+ current_rss = get_rss
+ return unless current_rss > MAX_RSS
+
+ raise ShutdownWithoutRaise.new("current RSS #{current_rss} exceeds maximum RSS #{MAX_RSS}")
+ end
+
+ def get_rss
+ output, status = Gitlab::Popen.popen(%W(ps -o rss= -p #{pid}), Rails.root.to_s)
+ return 0 unless status.zero?
+
+ output.to_i
+ end
+
+ def wait_and_signal(time, signal, explanation)
+ Sidekiq.logger.warn "waiting #{time} seconds before sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ sleep(time)
+
+ Sidekiq.logger.warn "sending Sidekiq worker PID-#{pid} #{signal} (#{explanation})"
+ kill(signal, pid)
+ end
+
+ def pid
+ Process.pid
+ end
+
+ def sleep(time)
+ if Rails.env.test?
+ @trace << [:sleep, time]
+ else
+ Kernel.sleep(time)
+ end
+ end
+
+ def kill(signal, pid)
+ if Rails.env.test?
+ @trace << [:kill, signal, pid]
+ else
+ Process.kill(signal, pid)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/slash_commands/base_command.rb b/lib/gitlab/slash_commands/base_command.rb
index cc3c9a50555..466554e398c 100644
--- a/lib/gitlab/slash_commands/base_command.rb
+++ b/lib/gitlab/slash_commands/base_command.rb
@@ -31,10 +31,11 @@ module Gitlab
raise NotImplementedError
end
- attr_accessor :project, :current_user, :params
+ attr_accessor :project, :current_user, :params, :chat_name
- def initialize(project, user, params = {})
- @project, @current_user, @params = project, user, params.dup
+ def initialize(project, chat_name, params = {})
+ @project, @current_user, @params = project, chat_name.user, params.dup
+ @chat_name = chat_name
end
private
diff --git a/lib/gitlab/slash_commands/command.rb b/lib/gitlab/slash_commands/command.rb
index a78408b0519..85aaa6b0eba 100644
--- a/lib/gitlab/slash_commands/command.rb
+++ b/lib/gitlab/slash_commands/command.rb
@@ -13,12 +13,13 @@ module Gitlab
if command
if command.allowed?(project, current_user)
- command.new(project, current_user, params).execute(match)
+ command.new(project, chat_name, params).execute(match)
else
Gitlab::SlashCommands::Presenters::Access.new.access_denied
end
else
- Gitlab::SlashCommands::Help.new(project, current_user, params).execute(available_commands, params[:text])
+ Gitlab::SlashCommands::Help.new(project, chat_name, params)
+ .execute(available_commands, params[:text])
end
end
diff --git a/lib/gitlab/string_placeholder_replacer.rb b/lib/gitlab/string_placeholder_replacer.rb
new file mode 100644
index 00000000000..9a2219b7d77
--- /dev/null
+++ b/lib/gitlab/string_placeholder_replacer.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ class StringPlaceholderReplacer
+ # This method accepts the following paras
+ # - string: the string to be analyzed
+ # - placeholder_regex: i.e. /%{project_path|project_id|default_branch|commit_sha}/
+ # - block: this block will be called with each placeholder found in the string using
+ # the placeholder regex. If the result of the block is nil, the original
+ # placeholder will be returned.
+
+ def self.replace_string_placeholders(string, placeholder_regex = nil, &block)
+ return string if string.blank? || placeholder_regex.blank? || !block_given?
+
+ replace_placeholders(string, placeholder_regex, &block)
+ end
+
+ class << self
+ private
+
+ # If the result of the block is nil, then the placeholder is returned
+ def replace_placeholders(string, placeholder_regex, &block)
+ string.gsub(/%{(#{placeholder_regex})}/) do |arg|
+ yield($~[1]) || arg
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/string_range_marker.rb b/lib/gitlab/string_range_marker.rb
index f9faa134206..c6ad997a4d4 100644
--- a/lib/gitlab/string_range_marker.rb
+++ b/lib/gitlab/string_range_marker.rb
@@ -14,7 +14,7 @@ module Gitlab
end
def mark(marker_ranges)
- return rich_line unless marker_ranges
+ return rich_line unless marker_ranges&.any?
if html_escaped
rich_marker_ranges = []
diff --git a/lib/gitlab/string_regex_marker.rb b/lib/gitlab/string_regex_marker.rb
index 7ebf1c0428c..b19aa6dea35 100644
--- a/lib/gitlab/string_regex_marker.rb
+++ b/lib/gitlab/string_regex_marker.rb
@@ -1,13 +1,15 @@
module Gitlab
class StringRegexMarker < StringRangeMarker
def mark(regex, group: 0, &block)
- regex_match = raw_line.match(regex)
- return rich_line unless regex_match
+ ranges = []
- begin_index, end_index = regex_match.offset(group)
- name_range = begin_index..(end_index - 1)
+ raw_line.scan(regex) do
+ begin_index, end_index = Regexp.last_match.offset(group)
- super([name_range], &block)
+ ranges << (begin_index..(end_index - 1))
+ end
+
+ super(ranges, &block)
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 9d13d1d781f..37d3512990e 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -9,6 +9,7 @@ module Gitlab
license_usage_data.merge(system_usage_data)
.merge(features_usage_data)
.merge(components_usage_data)
+ .merge(cycle_analytics_usage_data)
end
def to_json(force_refresh: false)
@@ -71,6 +72,10 @@ module Gitlab
}
end
+ def cycle_analytics_usage_data
+ Gitlab::CycleAnalytics::UsageData.new.to_json
+ end
+
def features_usage_data
features_usage_data_ce
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index ff4dc29efea..91b8bb2a83f 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -31,7 +31,7 @@ module Gitlab
return false unless can_access_git?
if user.requires_ldap_check? && user.try_obtain_ldap_lease
- return false unless Gitlab::LDAP::Access.allowed?(user)
+ return false unless Gitlab::Auth::LDAP::Access.allowed?(user)
end
true
diff --git a/lib/gitlab/verify/batch_verifier.rb b/lib/gitlab/verify/batch_verifier.rb
new file mode 100644
index 00000000000..1ef369a4b67
--- /dev/null
+++ b/lib/gitlab/verify/batch_verifier.rb
@@ -0,0 +1,64 @@
+module Gitlab
+ module Verify
+ class BatchVerifier
+ attr_reader :batch_size, :start, :finish
+
+ def initialize(batch_size:, start: nil, finish: nil)
+ @batch_size = batch_size
+ @start = start
+ @finish = finish
+ end
+
+ # Yields a Range of IDs and a Hash of failed verifications (object => error)
+ def run_batches(&blk)
+ relation.in_batches(of: batch_size, start: start, finish: finish) do |relation| # rubocop: disable Cop/InBatches
+ range = relation.first.id..relation.last.id
+ failures = run_batch(relation)
+
+ yield(range, failures)
+ end
+ end
+
+ def name
+ raise NotImplementedError.new
+ end
+
+ def describe(_object)
+ raise NotImplementedError.new
+ end
+
+ private
+
+ def run_batch(relation)
+ relation.map { |upload| verify(upload) }.compact.to_h
+ end
+
+ def verify(object)
+ expected = expected_checksum(object)
+ actual = actual_checksum(object)
+
+ raise 'Checksum missing' unless expected.present?
+ raise 'Checksum mismatch' unless expected == actual
+
+ nil
+ rescue => err
+ [object, err]
+ end
+
+ # This should return an ActiveRecord::Relation suitable for calling #in_batches on
+ def relation
+ raise NotImplementedError.new
+ end
+
+ # The checksum we expect the object to have
+ def expected_checksum(_object)
+ raise NotImplementedError.new
+ end
+
+ # The freshly-recalculated checksum of the object
+ def actual_checksum(_object)
+ raise NotImplementedError.new
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/verify/lfs_objects.rb b/lib/gitlab/verify/lfs_objects.rb
new file mode 100644
index 00000000000..fe51edbdeeb
--- /dev/null
+++ b/lib/gitlab/verify/lfs_objects.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Verify
+ class LfsObjects < BatchVerifier
+ def name
+ 'LFS objects'
+ end
+
+ def describe(object)
+ "LFS object: #{object.oid}"
+ end
+
+ private
+
+ def relation
+ LfsObject.all
+ end
+
+ def expected_checksum(lfs_object)
+ lfs_object.oid
+ end
+
+ def actual_checksum(lfs_object)
+ LfsObject.calculate_oid(lfs_object.file.path)
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/verify/rake_task.rb b/lib/gitlab/verify/rake_task.rb
new file mode 100644
index 00000000000..dd138e6b92b
--- /dev/null
+++ b/lib/gitlab/verify/rake_task.rb
@@ -0,0 +1,53 @@
+module Gitlab
+ module Verify
+ class RakeTask
+ def self.run!(verify_kls)
+ verifier = verify_kls.new(
+ batch_size: ENV.fetch('BATCH', 200).to_i,
+ start: ENV['ID_FROM'],
+ finish: ENV['ID_TO']
+ )
+
+ verbose = Gitlab::Utils.to_boolean(ENV['VERBOSE'])
+
+ new(verifier, verbose).run!
+ end
+
+ attr_reader :verifier, :output
+
+ def initialize(verifier, verbose)
+ @verifier = verifier
+ @verbose = verbose
+ end
+
+ def run!
+ say "Checking integrity of #{verifier.name}"
+
+ verifier.run_batches { |*args| run_batch(*args) }
+
+ say 'Done!'
+ end
+
+ def verbose?
+ !!@verbose
+ end
+
+ private
+
+ def say(text)
+ puts(text) # rubocop:disable Rails/Output
+ end
+
+ def run_batch(range, failures)
+ status_color = failures.empty? ? :green : :red
+ say "- #{range}: Failures: #{failures.count}".color(status_color)
+
+ return unless verbose?
+
+ failures.each do |object, error|
+ say " - #{verifier.describe(object)}: #{error.inspect}".color(:red)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/verify/uploads.rb b/lib/gitlab/verify/uploads.rb
new file mode 100644
index 00000000000..6972e517ea5
--- /dev/null
+++ b/lib/gitlab/verify/uploads.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Verify
+ class Uploads < BatchVerifier
+ def name
+ 'Uploads'
+ end
+
+ def describe(object)
+ "Upload: #{object.id}"
+ end
+
+ private
+
+ def relation
+ Upload.all
+ end
+
+ def expected_checksum(upload)
+ upload.checksum
+ end
+
+ def actual_checksum(upload)
+ Upload.hexdigest(upload.absolute_path)
+ end
+ end
+ end
+end
diff --git a/lib/haml_lint/inline_javascript.rb b/lib/haml_lint/inline_javascript.rb
index f5485eb89fa..4f776330e80 100644
--- a/lib/haml_lint/inline_javascript.rb
+++ b/lib/haml_lint/inline_javascript.rb
@@ -12,6 +12,12 @@ unless Rails.env.production?
record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
end
+
+ def visit_tag(node)
+ return unless node.tag_name == 'script'
+
+ record_lint(node, 'Inline JavaScript is discouraged (https://docs.gitlab.com/ee/development/gotchas.html#do-not-use-inline-javascript-in-views)')
+ end
end
end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index e05a3aad824..2403f57f05a 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -336,7 +336,7 @@ namespace :gitlab do
warn_user_is_not_gitlab
start_checking "LDAP"
- if Gitlab::LDAP::Config.enabled?
+ if Gitlab::Auth::LDAP::Config.enabled?
check_ldap(args.limit)
else
puts 'LDAP is disabled in config/gitlab.yml'
@@ -346,13 +346,13 @@ namespace :gitlab do
end
def check_ldap(limit)
- servers = Gitlab::LDAP::Config.providers
+ servers = Gitlab::Auth::LDAP::Config.providers
servers.each do |server|
puts "Server: #{server}"
begin
- Gitlab::LDAP::Adapter.open(server) do |adapter|
+ Gitlab::Auth::LDAP::Adapter.open(server) do |adapter|
check_ldap_auth(adapter)
puts "LDAP users with access to your GitLab server (only showing the first #{limit} results)"
diff --git a/lib/tasks/gitlab/cleanup.rake b/lib/tasks/gitlab/cleanup.rake
index 5a53eac0897..2453079911d 100644
--- a/lib/tasks/gitlab/cleanup.rake
+++ b/lib/tasks/gitlab/cleanup.rake
@@ -87,7 +87,7 @@ namespace :gitlab do
print "#{user.name} (#{user.ldap_identity.extern_uid}) ..."
- if Gitlab::LDAP::Access.allowed?(user)
+ if Gitlab::Auth::LDAP::Access.allowed?(user)
puts " [OK]".color(:green)
else
if block_flag
diff --git a/lib/tasks/gitlab/lfs/check.rake b/lib/tasks/gitlab/lfs/check.rake
new file mode 100644
index 00000000000..869463d4e5d
--- /dev/null
+++ b/lib/tasks/gitlab/lfs/check.rake
@@ -0,0 +1,8 @@
+namespace :gitlab do
+ namespace :lfs do
+ desc 'GitLab | LFS | Check integrity of uploaded LFS objects'
+ task check: :environment do
+ Gitlab::Verify::RakeTask.run!(Gitlab::Verify::LfsObjects)
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/uploads.rake b/lib/tasks/gitlab/uploads.rake
deleted file mode 100644
index df31567ce64..00000000000
--- a/lib/tasks/gitlab/uploads.rake
+++ /dev/null
@@ -1,44 +0,0 @@
-namespace :gitlab do
- namespace :uploads do
- desc 'GitLab | Uploads | Check integrity of uploaded files'
- task check: :environment do
- puts 'Checking integrity of uploaded files'
-
- uploads_batches do |batch|
- batch.each do |upload|
- puts "- Checking file (#{upload.id}): #{upload.absolute_path}".color(:green)
-
- if upload.exist?
- check_checksum(upload)
- else
- puts " * File does not exist on the file system".color(:red)
- end
- end
- end
-
- puts 'Done!'
- end
-
- def batch_size
- ENV.fetch('BATCH', 200).to_i
- end
-
- def calculate_checksum(absolute_path)
- Digest::SHA256.file(absolute_path).hexdigest
- end
-
- def check_checksum(upload)
- checksum = calculate_checksum(upload.absolute_path)
-
- if checksum != upload.checksum
- puts " * File checksum (#{checksum}) does not match the one in the database (#{upload.checksum})".color(:red)
- end
- end
-
- def uploads_batches(&block)
- Upload.all.in_batches(of: batch_size, start: ENV['ID_FROM'], finish: ENV['ID_TO']) do |relation| # rubocop: disable Cop/InBatches
- yield relation
- end
- end
- end
-end
diff --git a/lib/tasks/gitlab/uploads/check.rake b/lib/tasks/gitlab/uploads/check.rake
new file mode 100644
index 00000000000..2be2ec7f9c9
--- /dev/null
+++ b/lib/tasks/gitlab/uploads/check.rake
@@ -0,0 +1,8 @@
+namespace :gitlab do
+ namespace :uploads do
+ desc 'GitLab | Uploads | Check integrity of uploaded files'
+ task check: :environment do
+ Gitlab::Verify::RakeTask.run!(Gitlab::Verify::Uploads)
+ end
+ end
+end
diff --git a/lib/tasks/plugins.rake b/lib/tasks/plugins.rake
new file mode 100644
index 00000000000..e73dd7e68df
--- /dev/null
+++ b/lib/tasks/plugins.rake
@@ -0,0 +1,16 @@
+namespace :plugins do
+ desc 'Validate existing plugins'
+ task validate: :environment do
+ puts 'Validating plugins from /plugins directory'
+
+ Gitlab::Plugin.files.each do |file|
+ success, message = Gitlab::Plugin.execute(file, Gitlab::DataBuilder::Push::SAMPLE_DATA)
+
+ if success
+ puts "* #{file} succeed (zero exit code)."
+ else
+ puts "* #{file} failure (non-zero exit code). #{message}"
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 889a03e7859..8a2176a4d72 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -8,8 +8,8 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2018-02-20 10:26+0100\n"
-"PO-Revision-Date: 2018-02-20 10:26+0100\n"
+"POT-Creation-Date: 2018-03-05 13:02-0600\n"
+"PO-Revision-Date: 2018-03-05 13:02-0600\n"
"Last-Translator: FULL NAME <EMAIL@ADDRESS>\n"
"Language-Team: LANGUAGE <LL@li.org>\n"
"Language: \n"
@@ -48,6 +48,9 @@ msgid_plural "%s additional commits have been omitted to prevent performance iss
msgstr[0] ""
msgstr[1] ""
+msgid "%{actionText} & %{openOrClose} %{noteable}"
+msgstr ""
+
msgid "%{commit_author_link} authored %{commit_timeago}"
msgstr ""
@@ -65,6 +68,9 @@ msgstr ""
msgid "%{number_of_failures} of %{maximum_failures} failures. GitLab will not retry automatically. Reset storage information when the problem is resolved."
msgstr ""
+msgid "%{openOrClose} %{noteable}"
+msgstr ""
+
msgid "%{storage_name}: failed storage access attempt on host:"
msgid_plural "%{storage_name}: %{failed_attempts} failed storage access attempts:"
msgstr[0] ""
@@ -96,6 +102,9 @@ msgstr ""
msgid "A collection of graphs regarding Continuous Integration"
msgstr ""
+msgid "A project is where you house your files (repository), plan your work (issues), and publish your documentation (wiki), %{among_other_things_link}."
+msgstr ""
+
msgid "About auto deploy"
msgstr ""
@@ -123,9 +132,15 @@ msgstr ""
msgid "Add Contribution guide"
msgstr ""
+msgid "Add Kubernetes cluster"
+msgstr ""
+
msgid "Add License"
msgstr ""
+msgid "Add Readme"
+msgstr ""
+
msgid "Add new directory"
msgstr ""
@@ -144,7 +159,7 @@ msgstr ""
msgid "AdminArea|Stopping jobs failed"
msgstr ""
-msgid "AdminArea|You’re about to stop all jobs. This will halt all current jobs that are running."
+msgid "AdminArea|You’re about to stop all jobs.This will halt all current jobs that are running."
msgstr ""
msgid "AdminHealthPageLink|health page"
@@ -189,6 +204,9 @@ msgstr ""
msgid "All"
msgstr ""
+msgid "All features are enabled for blank projects, from templates, or when importing, but you can disable them afterward in the project settings."
+msgstr ""
+
msgid "Allows you to add and manage Kubernetes clusters."
msgstr ""
@@ -207,6 +225,9 @@ msgstr ""
msgid "An error occurred while fetching sidebar data"
msgstr ""
+msgid "An error occurred while fetching the pipeline."
+msgstr ""
+
msgid "An error occurred while getting projects"
msgstr ""
@@ -225,6 +246,9 @@ msgstr ""
msgid "An error occurred while loading the file"
msgstr ""
+msgid "An error occurred while making the request."
+msgstr ""
+
msgid "An error occurred while rendering KaTeX"
msgstr ""
@@ -312,6 +336,9 @@ msgstr ""
msgid "Authors: %{authors}"
msgstr ""
+msgid "Auto DevOps enabled"
+msgstr ""
+
msgid "Auto Review Apps and Auto Deploy need a %{kubernetes} to work correctly."
msgstr ""
@@ -336,7 +363,13 @@ msgstr ""
msgid "AutoDevOps|Learn more in the %{link_to_documentation}"
msgstr ""
-msgid "AutoDevOps|You can activate %{link_to_settings} for this project."
+msgid "AutoDevOps|You can automatically build and test your application if you %{link_to_auto_devops_settings} for this project. You can automatically deploy it as well, if you %{link_to_add_kubernetes_cluster}."
+msgstr ""
+
+msgid "AutoDevOps|add a Kubernetes cluster"
+msgstr ""
+
+msgid "AutoDevOps|enable Auto DevOps (Beta)"
msgstr ""
msgid "Available"
@@ -351,8 +384,8 @@ msgstr ""
msgid "Begin with the selected commit"
msgstr ""
-msgid "Branch"
-msgid_plural "Branches"
+msgid "Branch (%{branch_count})"
+msgid_plural "Branches (%{branch_count})"
msgstr[0] ""
msgstr[1] ""
@@ -620,6 +653,9 @@ msgstr ""
msgid "CircuitBreakerApiLink|circuitbreaker api"
msgstr ""
+msgid "Click the button below to begin the install process by navigating to the Kubernetes page"
+msgstr ""
+
msgid "Click to expand text"
msgstr ""
@@ -668,6 +704,9 @@ msgstr ""
msgid "ClusterIntegration|Copy CA Certificate"
msgstr ""
+msgid "ClusterIntegration|Copy Ingress IP Address to clipboard"
+msgstr ""
+
msgid "ClusterIntegration|Copy Kubernetes cluster name"
msgstr ""
@@ -716,6 +755,9 @@ msgstr ""
msgid "ClusterIntegration|Ingress"
msgstr ""
+msgid "ClusterIntegration|Ingress IP Address"
+msgstr ""
+
msgid "ClusterIntegration|Install"
msgstr ""
@@ -767,9 +809,6 @@ msgstr ""
msgid "ClusterIntegration|Learn more about %{link_to_documentation}"
msgstr ""
-msgid "ClusterIntegration|Learn more about Kubernetes"
-msgstr ""
-
msgid "ClusterIntegration|Learn more about environments"
msgstr ""
@@ -899,6 +938,12 @@ msgstr ""
msgid "ClusterIntegration|properly configured"
msgstr ""
+msgid "Comment and resolve discussion"
+msgstr ""
+
+msgid "Comment and unresolve discussion"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -907,6 +952,11 @@ msgid_plural "Commits"
msgstr[0] ""
msgstr[1] ""
+msgid "Commit (%{commit_count})"
+msgid_plural "Commits (%{commit_count})"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Commit Message"
msgstr ""
@@ -1051,6 +1101,9 @@ msgstr ""
msgid "Copy branch name to clipboard"
msgstr ""
+msgid "Copy command to clipboard"
+msgstr ""
+
msgid "Copy commit SHA to clipboard"
msgstr ""
@@ -1233,6 +1286,9 @@ msgstr ""
msgid "Emails"
msgstr ""
+msgid "Enable Auto DevOps"
+msgstr ""
+
msgid "Environments|An error occurred while fetching the environments."
msgstr ""
@@ -1341,6 +1397,9 @@ msgstr ""
msgid "Explore public groups"
msgstr ""
+msgid "Failed Jobs"
+msgstr ""
+
msgid "Failed to change the owner"
msgstr ""
@@ -1368,6 +1427,9 @@ msgstr ""
msgid "Files"
msgstr ""
+msgid "Files (%{human_size})"
+msgstr ""
+
msgid "Filter by commit message"
msgstr ""
@@ -1394,6 +1456,9 @@ msgstr ""
msgid "ForkedFromProjectPath|Forked from %{project_name} (deleted)"
msgstr ""
+msgid "Forking in progress"
+msgstr ""
+
msgid "Format"
msgstr ""
@@ -1403,12 +1468,18 @@ msgstr ""
msgid "From merge request merge until deploy to production"
msgstr ""
+msgid "From the Kubernetes cluster details view, install Runner from the applications list"
+msgstr ""
+
msgid "GPG Keys"
msgstr ""
msgid "Generate a default set of labels"
msgstr ""
+msgid "Git repository URL"
+msgstr ""
+
msgid "Git revision"
msgstr ""
@@ -1531,9 +1602,21 @@ msgstr ""
msgid "Housekeeping successfully started"
msgstr ""
+msgid "If you already have files you can push them using the %{link_to_cli} below."
+msgstr ""
+
+msgid "If your HTTP repository is not publicly accessible, add authentication information to the URL: <code>https://username:password@gitlab.company.com/group/project.git</code>."
+msgstr ""
+
+msgid "Import in progress"
+msgstr ""
+
msgid "Import repository"
msgstr ""
+msgid "Install Runner on Kubernetes"
+msgstr ""
+
msgid "Install a Runner compatible with GitLab CI"
msgstr ""
@@ -1573,6 +1656,9 @@ msgstr ""
msgid "January"
msgstr ""
+msgid "Jobs"
+msgstr ""
+
msgid "Jul"
msgstr ""
@@ -1603,6 +1689,9 @@ msgstr ""
msgid "Kubernetes cluster was successfully updated."
msgstr ""
+msgid "Kubernetes configured"
+msgstr ""
+
msgid "Kubernetes service integration has been deprecated. %{deprecated_message_content} your Kubernetes clusters using the new <a href=\"%{url}\"/>Kubernetes Clusters</a> page"
msgstr ""
@@ -1650,6 +1739,12 @@ msgstr ""
msgid "Learn more"
msgstr ""
+msgid "Learn more about Kubernetes"
+msgstr ""
+
+msgid "Learn more about protected branches"
+msgstr ""
+
msgid "Learn more in the"
msgstr ""
@@ -1743,9 +1838,18 @@ msgstr ""
msgid "MissingSSHKeyWarningLink|add an SSH key"
msgstr ""
+msgid "Modal|Cancel"
+msgstr ""
+
+msgid "Modal|Close"
+msgstr ""
+
msgid "Monitoring"
msgstr ""
+msgid "More information"
+msgstr ""
+
msgid "More information is available|here"
msgstr ""
@@ -1832,9 +1936,6 @@ msgstr ""
msgid "No schedules"
msgstr ""
-msgid "No time spent"
-msgstr ""
-
msgid "None"
msgstr ""
@@ -1850,6 +1951,9 @@ msgstr ""
msgid "Not enough data"
msgstr ""
+msgid "Note that the master branch is automatically protected. %{link_to_protected_branches}"
+msgstr ""
+
msgid "Notification events"
msgstr ""
@@ -1943,6 +2047,9 @@ msgstr ""
msgid "Options"
msgstr ""
+msgid "Otherwise it is recommended you start with one of the options below."
+msgstr ""
+
msgid "Overview"
msgstr ""
@@ -2048,19 +2155,19 @@ msgstr ""
msgid "Pipeline|Retry pipeline"
msgstr ""
-msgid "Pipeline|Retry pipeline #%{id}?"
+msgid "Pipeline|Retry pipeline #%{pipelineId}?"
msgstr ""
msgid "Pipeline|Stop pipeline"
msgstr ""
-msgid "Pipeline|Stop pipeline #%{id}?"
+msgid "Pipeline|Stop pipeline #%{pipelineId}?"
msgstr ""
-msgid "Pipeline|You’re about to retry pipeline %{id}."
+msgid "Pipeline|You’re about to retry pipeline %{pipelineId}."
msgstr ""
-msgid "Pipeline|You’re about to stop pipeline %{id}."
+msgid "Pipeline|You’re about to stop pipeline %{pipelineId}."
msgstr ""
msgid "Pipeline|all"
@@ -2084,6 +2191,9 @@ msgstr ""
msgid "Please solve the reCAPTCHA"
msgstr ""
+msgid "Please wait while we import the repository for you. Refresh at will."
+msgstr ""
+
msgid "Preferences"
msgstr ""
@@ -2093,6 +2203,9 @@ msgstr ""
msgid "Private - The group and its projects can only be viewed by members."
msgstr ""
+msgid "Private projects can be created in your personal namespace with:"
+msgstr ""
+
msgid "Profile"
msgstr ""
@@ -2294,6 +2407,12 @@ msgstr ""
msgid "Push events"
msgstr ""
+msgid "Push project from command line"
+msgstr ""
+
+msgid "Push to create a project"
+msgstr ""
+
msgid "Quick actions can be used in the issues description and comment boxes."
msgstr ""
@@ -2357,6 +2476,9 @@ msgstr ""
msgid "Reset runners registration token"
msgstr ""
+msgid "Resolve discussion"
+msgstr ""
+
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
@@ -2416,6 +2538,9 @@ msgstr ""
msgid "Select a timezone"
msgstr ""
+msgid "Select an existing Kubernetes cluster or create a new one"
+msgstr ""
+
msgid "Select assignee"
msgstr ""
@@ -2425,6 +2550,9 @@ msgstr ""
msgid "Select target branch"
msgstr ""
+msgid "Send email"
+msgstr ""
+
msgid "Sep"
msgstr ""
@@ -2446,15 +2574,18 @@ msgstr ""
msgid "Set up Koding"
msgstr ""
-msgid "Set up auto deploy"
-msgstr ""
-
msgid "SetPasswordToCloneLink|set a password"
msgstr ""
msgid "Settings"
msgstr ""
+msgid "Setup a specific Runner automatically"
+msgstr ""
+
+msgid "Show command"
+msgstr ""
+
msgid "Show parent pages"
msgstr ""
@@ -2478,10 +2609,13 @@ msgstr ""
msgid "Something went wrong trying to change the confidentiality of this issue"
msgstr ""
+msgid "Something went wrong trying to change the locked state of this ${this.issuableDisplayName}"
+msgstr ""
+
msgid "Something went wrong when toggling the button"
msgstr ""
-msgid "Something went wrong while closing the issue. Please try again later"
+msgid "Something went wrong while closing the %{issuable}. Please try again later"
msgstr ""
msgid "Something went wrong while fetching the projects."
@@ -2490,7 +2624,10 @@ msgstr ""
msgid "Something went wrong while fetching the registry list."
msgstr ""
-msgid "Something went wrong while reopening the issue. Please try again later"
+msgid "Something went wrong while reopening the %{issuable}. Please try again later"
+msgstr ""
+
+msgid "Something went wrong while resolving this discussion. Please try again."
msgstr ""
msgid "Something went wrong. Please try again."
@@ -2631,8 +2768,8 @@ msgstr ""
msgid "System Hooks"
msgstr ""
-msgid "Tag"
-msgid_plural "Tags"
+msgid "Tag (%{tag_count})"
+msgid_plural "Tags (%{tag_count})"
msgstr[0] ""
msgstr[1] ""
@@ -2729,6 +2866,9 @@ msgstr ""
msgid "The fork relationship has been removed."
msgstr ""
+msgid "The import will time out after %{timeout}. For repositories that take longer, use a clone/push combination."
+msgstr ""
+
msgid "The issue stage shows the time it takes from creating an issue to assigning the issue to a milestone, or add the issue to a list on your Issue Board. Begin creating issues to see data for this stage."
msgstr ""
@@ -2759,6 +2899,12 @@ msgstr ""
msgid "The repository for this project does not exist."
msgstr ""
+msgid "The repository for this project is empty"
+msgstr ""
+
+msgid "The repository must be accessible over <code>http://</code>, <code>https://</code> or <code>git://</code>."
+msgstr ""
+
msgid "The review stage shows the time from creating the merge request to merging it. The data will automatically be added after you merge your first merge request."
msgstr ""
@@ -3021,6 +3167,12 @@ msgstr[1] ""
msgid "Time|s"
msgstr ""
+msgid "Tip:"
+msgstr ""
+
+msgid "To import an SVN repository, check out %{svn_link}."
+msgstr ""
+
msgid "Todo"
msgstr ""
@@ -3036,9 +3188,6 @@ msgstr ""
msgid "Total Time"
msgstr ""
-msgid "Total issue time spent"
-msgstr ""
-
msgid "Total test time for all commits/merges"
msgstr ""
@@ -3063,6 +3212,9 @@ msgstr ""
msgid "Unlocked"
msgstr ""
+msgid "Unresolve discussion"
+msgstr ""
+
msgid "Unstar"
msgstr ""
@@ -3126,6 +3278,9 @@ msgstr ""
msgid "We want to be sure it is you, please confirm you are not a robot."
msgstr ""
+msgid "Web IDE"
+msgstr ""
+
msgid "Wiki"
msgstr ""
@@ -3252,9 +3407,15 @@ msgstr ""
msgid "You are on a read-only GitLab instance."
msgstr ""
+msgid "You can also create a project from the command line."
+msgstr ""
+
msgid "You can also star a label to make it a priority label."
msgstr ""
+msgid "You can easily install a Runner on a Kubernetes cluster. %{link_to_help_page}"
+msgstr ""
+
msgid "You can move around the graph by using the arrow keys."
msgstr ""
@@ -3318,12 +3479,18 @@ msgstr ""
msgid "Your projects"
msgstr ""
+msgid "among other things"
+msgstr ""
+
msgid "assign yourself"
msgstr ""
msgid "branch name"
msgstr ""
+msgid "command line instructions"
+msgstr ""
+
msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
msgstr ""
@@ -3510,6 +3677,9 @@ msgstr ""
msgid "spendCommand|%{slash_command} will update the sum of the time spent."
msgstr ""
+msgid "this document"
+msgstr ""
+
msgid "username"
msgstr ""
diff --git a/package.json b/package.json
index 043af80a3be..cbad55b4c85 100644
--- a/package.json
+++ b/package.json
@@ -1,7 +1,7 @@
{
"private": true,
"scripts": {
- "dev-server": "nodemon -w 'config/webpack.config.js' -w 'app/assets/javascripts/dispatcher.js' -w 'app/assets/javascripts/pages/**/index.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
+ "dev-server": "nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"eslint": "eslint --max-warnings 0 --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html .",
@@ -116,6 +116,6 @@
"karma-webpack": "2.0.7",
"nodemon": "^1.15.1",
"prettier": "1.9.2",
- "webpack-dev-server": "^2.11.1"
+ "webpack-dev-server": "^2.11.2"
}
}
diff --git a/plugins/examples/save_to_file.clj b/plugins/examples/save_to_file.clj
new file mode 100755
index 00000000000..a59d83749d3
--- /dev/null
+++ b/plugins/examples/save_to_file.clj
@@ -0,0 +1,3 @@
+#!/usr/bin/env clojure
+(let [in (slurp *in*)]
+ (spit "/tmp/clj-data.txt" in))
diff --git a/plugins/examples/save_to_file.rb b/plugins/examples/save_to_file.rb
new file mode 100755
index 00000000000..61b0df9bfd6
--- /dev/null
+++ b/plugins/examples/save_to_file.rb
@@ -0,0 +1,3 @@
+#!/usr/bin/env ruby
+x = STDIN.read
+File.write('/tmp/rb-data.txt', x)
diff --git a/qa/qa.rb b/qa/qa.rb
index c6de8625f3d..7220af5088e 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -130,6 +130,7 @@ module QA
autoload :DeployKeys, 'qa/page/project/settings/deploy_keys'
autoload :SecretVariables, 'qa/page/project/settings/secret_variables'
autoload :Runners, 'qa/page/project/settings/runners'
+ autoload :MergeRequest, 'qa/page/project/settings/merge_request'
end
module Issue
@@ -145,6 +146,7 @@ module QA
module MergeRequest
autoload :New, 'qa/page/merge_request/new'
+ autoload :Show, 'qa/page/merge_request/show'
end
module Admin
diff --git a/qa/qa/factory/base.rb b/qa/qa/factory/base.rb
index bd66b74a164..afaa96b4541 100644
--- a/qa/qa/factory/base.rb
+++ b/qa/qa/factory/base.rb
@@ -22,7 +22,7 @@ module QA
factory.fabricate!(*args)
- return Factory::Product.populate!(self)
+ return Factory::Product.populate!(factory)
end
end
diff --git a/qa/qa/factory/product.rb b/qa/qa/factory/product.rb
index d004e642f9b..996b7f14f61 100644
--- a/qa/qa/factory/product.rb
+++ b/qa/qa/factory/product.rb
@@ -17,8 +17,9 @@ module QA
def self.populate!(factory)
new.tap do |product|
- factory.attributes.each_value do |attribute|
- product.instance_exec(&attribute.block).tap do |value|
+ factory.class.attributes.each_value do |attribute|
+ product.instance_exec(factory, attribute.block) do |factory, block|
+ value = block.call(factory)
product.define_singleton_method(attribute.name) { value }
end
end
diff --git a/qa/qa/factory/repository/push.rb b/qa/qa/factory/repository/push.rb
index 2f4de4173d4..6e8905cde78 100644
--- a/qa/qa/factory/repository/push.rb
+++ b/qa/qa/factory/repository/push.rb
@@ -2,7 +2,7 @@ module QA
module Factory
module Repository
class Push < Factory::Base
- attr_writer :file_name, :file_content, :commit_message, :branch_name
+ attr_writer :file_name, :file_content, :commit_message, :branch_name, :new_branch
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-code'
@@ -14,6 +14,7 @@ module QA
@file_content = '# This is test project'
@commit_message = "Add #{@file_name}"
@branch_name = 'master'
+ @new_branch = true
end
def fabricate!
@@ -29,6 +30,7 @@ module QA
repository.clone
repository.configure_identity('GitLab QA', 'root@gitlab.com')
+ repository.checkout(@branch_name) unless @new_branch
repository.add_file(@file_name, @file_content)
repository.commit(@commit_message)
repository.push_changes(@branch_name)
diff --git a/qa/qa/factory/resource/merge_request.rb b/qa/qa/factory/resource/merge_request.rb
index ce04e904aaf..539fe6b8a70 100644
--- a/qa/qa/factory/resource/merge_request.rb
+++ b/qa/qa/factory/resource/merge_request.rb
@@ -9,11 +9,20 @@ module QA
:source_branch,
:target_branch
+ product :project do |factory|
+ factory.project
+ end
+
+ product :source_branch do |factory|
+ factory.source_branch
+ end
+
dependency Factory::Resource::Project, as: :project do |project|
project.name = 'project-with-merge-request'
end
dependency Factory::Repository::Push, as: :target do |push, factory|
+ factory.project.visit!
push.project = factory.project
push.branch_name = "master:#{factory.target_branch}"
end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 4c4ef3ef477..b3150e8f3fa 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -36,6 +36,10 @@ module QA
`git clone #{opts} #{@uri.to_s} ./ #{suppress_output}`
end
+ def checkout(branch_name)
+ `git checkout "#{branch_name}"`
+ end
+
def shallow_clone
clone('--depth 1')
end
diff --git a/qa/qa/page/merge_request/show.rb b/qa/qa/page/merge_request/show.rb
new file mode 100644
index 00000000000..35875487da8
--- /dev/null
+++ b/qa/qa/page/merge_request/show.rb
@@ -0,0 +1,46 @@
+module QA
+ module Page
+ module MergeRequest
+ class Show < Page::Base
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_ready_to_merge.js' do
+ element :merge_button
+ end
+
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_merged.vue' do
+ element :merged_status, 'The changes were merged into'
+ end
+
+ view 'app/assets/javascripts/vue_merge_request_widget/components/states/mr_widget_rebase.vue' do
+ element :mr_rebase_button
+ element :fast_forward_nessage, "Fast-forward merge is not possible"
+ end
+
+ def rebase!
+ wait(reload: false) do
+ click_element :mr_rebase_button
+
+ has_text?("The source branch HEAD has recently changed.")
+ end
+ end
+
+ def fast_forward_possible?
+ !has_text?("Fast-forward merge is not possible")
+ end
+
+ def has_merge_button?
+ refresh
+
+ has_selector?('.accept-merge-request')
+ end
+
+ def merge!
+ wait(reload: false) do
+ click_element :merge_button
+
+ has_text?("The changes were merged into")
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/page/project/settings/merge_request.rb b/qa/qa/page/project/settings/merge_request.rb
new file mode 100644
index 00000000000..b147c91b467
--- /dev/null
+++ b/qa/qa/page/project/settings/merge_request.rb
@@ -0,0 +1,27 @@
+module QA
+ module Page
+ module Project
+ module Settings
+ class MergeRequest < QA::Page::Base
+ include Common
+
+ view 'app/views/projects/_merge_request_fast_forward_settings.html.haml' do
+ element :radio_button_merge_ff
+ end
+
+ view 'app/views/projects/edit.html.haml' do
+ element :merge_request_settings, 'Merge request settings'
+ element :save_merge_request_changes
+ end
+
+ def enable_ff_only
+ expand_section('Merge request settings') do
+ click_element :radio_button_merge_ff
+ click_element :save_merge_request_changes
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb
new file mode 100644
index 00000000000..2a44d42af6f
--- /dev/null
+++ b/qa/qa/specs/features/merge_request/rebase_spec.rb
@@ -0,0 +1,39 @@
+module QA
+ feature 'merge request rebase', :core do
+ scenario 'rebases source branch of merge request' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ project = Factory::Resource::Project.fabricate! do |project|
+ project.name = "only-fast-forward"
+ end
+
+ Page::Menu::Side.act { go_to_settings }
+ Page::Project::Settings::MergeRequest.act { enable_ff_only }
+
+ merge_request = Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request.project = project
+ merge_request.title = 'Needs rebasing'
+ end
+
+ Factory::Repository::Push.fabricate! do |push|
+ push.project = project
+ push.file_name = "other.txt"
+ push.file_content = "New file added!"
+ end
+
+ merge_request.visit!
+
+ Page::MergeRequest::Show.perform do |merge_request|
+ expect(merge_request).to have_content('Needs rebasing')
+ expect(merge_request).not_to be_fast_forward_possible
+ expect(merge_request).not_to have_merge_button
+
+ merge_request.rebase!
+
+ expect(merge_request).to have_merge_button
+ expect(merge_request.fast_forward_possible?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/qa/spec/factory/base_spec.rb b/qa/spec/factory/base_spec.rb
index c5663049be8..04e04886699 100644
--- a/qa/spec/factory/base_spec.rb
+++ b/qa/spec/factory/base_spec.rb
@@ -7,6 +7,7 @@ describe QA::Factory::Base do
before do
allow(QA::Factory::Product).to receive(:new).and_return(product)
+ allow(QA::Factory::Product).to receive(:populate!).and_return(product)
end
it 'instantiates the factory and calls factory method' do
@@ -76,6 +77,7 @@ describe QA::Factory::Base do
allow(subject).to receive(:new).and_return(instance)
allow(instance).to receive(:mydep).and_return(nil)
allow(QA::Factory::Product).to receive(:new)
+ allow(QA::Factory::Product).to receive(:populate!)
end
it 'builds all dependencies first' do
@@ -89,8 +91,16 @@ describe QA::Factory::Base do
describe '.product' do
subject do
Class.new(described_class) do
+ def fabricate!
+ "any"
+ end
+
+ # Defined only to be stubbed
+ def self.find_page
+ end
+
product :token do
- page.do_something_on_page!
+ find_page.do_something_on_page!
'resulting value'
end
end
@@ -105,16 +115,17 @@ describe QA::Factory::Base do
let(:page) { spy('page') }
before do
- allow(subject).to receive(:new).and_return(factory)
+ allow(factory).to receive(:class).and_return(subject)
allow(QA::Factory::Product).to receive(:new).and_return(product)
allow(product).to receive(:page).and_return(page)
+ allow(subject).to receive(:find_page).and_return(page)
end
it 'populates product after fabrication' do
subject.fabricate!
- expect(page).to have_received(:do_something_on_page!)
expect(product.token).to eq 'resulting value'
+ expect(page).to have_received(:do_something_on_page!)
end
end
end
diff --git a/qa/spec/factory/product_spec.rb b/qa/spec/factory/product_spec.rb
index fdfb1ec90cc..f245aabbf43 100644
--- a/qa/spec/factory/product_spec.rb
+++ b/qa/spec/factory/product_spec.rb
@@ -1,9 +1,20 @@
describe QA::Factory::Product do
- let(:factory) { spy('factory') }
+ let(:factory) do
+ QA::Factory::Base.new
+ end
+
+ let(:attributes) do
+ { test: QA::Factory::Product::Attribute.new(:test, proc { 'returned' }) }
+ end
+
let(:product) { spy('product') }
+ before do
+ allow(QA::Factory::Base).to receive(:attributes).and_return(attributes)
+ end
+
describe '.populate!' do
- it 'returns a fabrication product' do
+ it 'returns a fabrication product and define factory attributes as its methods' do
expect(described_class).to receive(:new).and_return(product)
result = described_class.populate!(factory) do |instance|
@@ -11,6 +22,7 @@ describe QA::Factory::Product do
end
expect(result).to be product
+ expect(result.test).to eq('returned')
end
end
diff --git a/scripts/security-harness b/scripts/security-harness
index d454f44dff7..c60b3410095 100755
--- a/scripts/security-harness
+++ b/scripts/security-harness
@@ -21,6 +21,8 @@ else
File.open(hook_path, 'w') do |file|
IO.copy_stream(DATA, file)
end
+
+ File.chmod(0755, hook_path)
end
# Toggle the harness on or off
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
index a270823b857..ae8cac0cf02 100755
--- a/scripts/trigger-build-docs
+++ b/scripts/trigger-build-docs
@@ -7,7 +7,7 @@ require 'gitlab'
#
Gitlab.configure do |config|
config.endpoint = 'https://gitlab.com/api/v4'
- config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs
+ config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token with Developer access to gitlab-docs
end
#
@@ -31,13 +31,24 @@ def docs_branch
end
#
-# Create a remote branch in gitlab-docs
+# Create a remote branch in gitlab-docs and immediately cancel the pipeline
+# to avoid race conditions, since a triggered pipeline will also run right
+# after the branch creation. This only happens the very first time a branch
+# is created and will be skipped in subsequent runs. Read more in
+# https://gitlab.com/gitlab-com/gitlab-docs/issues/154.
#
def create_remote_branch
Gitlab.create_branch(GITLAB_DOCS_REPO, docs_branch, 'master')
- puts "Remote branch '#{docs_branch}' created"
+ puts "=> Remote branch '#{docs_branch}' created"
+
+ # Get the latest pipeline ID which is also the first
+ pipeline_id = Gitlab.pipelines(GITLAB_DOCS_REPO, { ref: docs_branch }).last.id
+
+ # Cancel the pipeline
+ Gitlab.cancel_pipeline(GITLAB_DOCS_REPO, pipeline_id)
+ puts "=> Canceled uneeded pipeline #{pipeline_id} for '#{docs_branch}'"
rescue Gitlab::Error::BadRequest
- puts "Remote branch '#{docs_branch}' already exists"
+ puts "=> Remote branch '#{docs_branch}' already exists"
end
#
@@ -45,7 +56,7 @@ end
#
def remove_remote_branch
Gitlab.delete_branch(GITLAB_DOCS_REPO, docs_branch)
- puts "Remote branch '#{docs_branch}' deleted"
+ puts "=> Remote branch '#{docs_branch}' deleted"
end
#
@@ -78,18 +89,22 @@ def trigger_pipeline
# The review app URL
app_url = "http://#{docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{slug}"
- # Create the pipeline
- puts "=> Triggering a pipeline..."
+ # Create the cross project pipeline using CI_JOB_TOKEN
pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["CI_JOB_TOKEN"], docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
- puts "=> Pipeline created:"
+ puts "=> Follow the status of the triggered pipeline:"
puts ""
puts "https://gitlab.com/gitlab-com/gitlab-docs/pipelines/#{pipeline.id}"
puts ""
- puts "=> Preview your changes live at:"
+ puts "=> In a few minutes, you will be able to preview your changes under the following URL:"
puts ""
puts app_url
puts ""
+ puts "=> For more information, read the documentation"
+ puts "=> https://docs.gitlab.com/ee/development/writing_documentation.html#previewing-the-changes-live"
+ puts ""
+ puts "=> If something doesn't work, drop a line in the #docs chat channel."
+ puts ""
end
#
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index b7257fac608..fb6d82d7de3 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -246,7 +246,7 @@ describe AutocompleteController do
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq authorized_project.id
- expect(json_response.first['name_with_namespace']).to eq authorized_project.name_with_namespace
+ expect(json_response.first['name_with_namespace']).to eq authorized_project.full_name
end
end
end
@@ -267,7 +267,7 @@ describe AutocompleteController do
expect(json_response.size).to eq(1)
expect(json_response.first['id']).to eq authorized_search_project.id
- expect(json_response.first['name_with_namespace']).to eq authorized_search_project.name_with_namespace
+ expect(json_response.first['name_with_namespace']).to eq authorized_search_project.full_name
end
end
end
diff --git a/spec/controllers/groups/labels_controller_spec.rb b/spec/controllers/groups/labels_controller_spec.rb
index da54aa9054c..185b6b4ce57 100644
--- a/spec/controllers/groups/labels_controller_spec.rb
+++ b/spec/controllers/groups/labels_controller_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Groups::LabelsController do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
+ set(:group) { create(:group) }
+ set(:user) { create(:user) }
+ set(:project) { create(:project, namespace: group) }
before do
group.add_owner(user)
@@ -10,6 +11,34 @@ describe Groups::LabelsController do
sign_in(user)
end
+ describe 'GET #index' do
+ set(:label_1) { create(:label, project: project, title: 'label_1') }
+ set(:group_label_1) { create(:group_label, group: group, title: 'group_label_1') }
+
+ it 'returns group and project labels by default' do
+ get :index, group_id: group, format: :json
+
+ label_ids = json_response.map {|label| label['title']}
+ expect(label_ids).to match_array([label_1.title, group_label_1.title])
+ end
+
+ context 'with ancestor group', :nested_groups do
+ set(:subgroup) { create(:group, parent: group) }
+ set(:subgroup_label_1) { create(:group_label, group: subgroup, title: 'subgroup_label_1') }
+
+ before do
+ subgroup.add_owner(user)
+ end
+
+ it 'returns ancestor group labels', :nested_groups do
+ get :index, group_id: subgroup, include_ancestor_groups: true, only_group_labels: true, format: :json
+
+ label_ids = json_response.map {|label| label['title']}
+ expect(label_ids).to match_array([group_label_1.title, subgroup_label_1.title])
+ end
+ end
+ end
+
describe 'POST #toggle_subscription' do
it 'allows user to toggle subscription on group labels' do
label = create(:group_label, group: group)
diff --git a/spec/controllers/oauth/authorizations_controller_spec.rb b/spec/controllers/oauth/authorizations_controller_spec.rb
index 004b463e745..149b690ff70 100644
--- a/spec/controllers/oauth/authorizations_controller_spec.rb
+++ b/spec/controllers/oauth/authorizations_controller_spec.rb
@@ -34,6 +34,8 @@ describe Oauth::AuthorizationsController do
end
context 'with valid params' do
+ render_views
+
it 'returns 200 code and renders view' do
get :new, params
diff --git a/spec/controllers/projects/branches_controller_spec.rb b/spec/controllers/projects/branches_controller_spec.rb
index 734396ddf7b..3b9e06cb5ad 100644
--- a/spec/controllers/projects/branches_controller_spec.rb
+++ b/spec/controllers/projects/branches_controller_spec.rb
@@ -407,10 +407,43 @@ describe Projects::BranchesController do
get :index,
namespace_id: project.namespace,
project_id: project,
+ state: 'all',
format: :html
expect(response).to have_gitlab_http_status(200)
end
end
+
+ context 'when depreated sort/search/page parameters are specified' do
+ it 'returns with a status 301 when sort specified' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ sort: 'updated_asc',
+ format: :html
+
+ expect(response).to redirect_to project_branches_filtered_path(project, state: 'all')
+ end
+
+ it 'returns with a status 301 when search specified' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ search: 'feature',
+ format: :html
+
+ expect(response).to redirect_to project_branches_filtered_path(project, state: 'all')
+ end
+
+ it 'returns with a status 301 when page specified' do
+ get :index,
+ namespace_id: project.namespace,
+ project_id: project,
+ page: 2,
+ format: :html
+
+ expect(response).to redirect_to project_branches_filtered_path(project, state: 'all')
+ end
+ end
end
end
diff --git a/spec/controllers/projects/clusters_controller_spec.rb b/spec/controllers/projects/clusters_controller_spec.rb
index 954fc79f57d..15ce418d0d6 100644
--- a/spec/controllers/projects/clusters_controller_spec.rb
+++ b/spec/controllers/projects/clusters_controller_spec.rb
@@ -91,6 +91,12 @@ describe Projects::ClustersController do
expect(response).to have_gitlab_http_status(:ok)
expect(response).to match_response_schema('cluster_status')
end
+
+ it 'invokes schedule_status_update on each application' do
+ expect_any_instance_of(Clusters::Applications::Ingress).to receive(:schedule_status_update)
+
+ go
+ end
end
describe 'security' do
diff --git a/spec/controllers/projects/cycle_analytics_controller_spec.rb b/spec/controllers/projects/cycle_analytics_controller_spec.rb
index 7c708a418a7..5516c95d044 100644
--- a/spec/controllers/projects/cycle_analytics_controller_spec.rb
+++ b/spec/controllers/projects/cycle_analytics_controller_spec.rb
@@ -27,7 +27,7 @@ describe Projects::CycleAnalyticsController do
milestone = create(:milestone, project: project, created_at: 5.days.ago)
issue.update(milestone: milestone)
- create_merge_request_closing_issue(issue)
+ create_merge_request_closing_issue(user, project, issue)
end
it 'is false' do
diff --git a/spec/controllers/projects/discussions_controller_spec.rb b/spec/controllers/projects/discussions_controller_spec.rb
index 00328d3ea51..fcb0c2f28c8 100644
--- a/spec/controllers/projects/discussions_controller_spec.rb
+++ b/spec/controllers/projects/discussions_controller_spec.rb
@@ -71,6 +71,19 @@ describe Projects::DiscussionsController do
expect(response).to have_gitlab_http_status(200)
end
+
+ context "when vue_mr_discussions cookie is present" do
+ before do
+ allow(controller).to receive(:cookies).and_return(vue_mr_discussions: 'true')
+ end
+
+ it "renders discussion with serializer" do
+ expect_any_instance_of(DiscussionSerializer).to receive(:represent)
+ .with(instance_of(Discussion), { context: instance_of(described_class) })
+
+ post :resolve, request_params
+ end
+ end
end
end
end
@@ -119,6 +132,19 @@ describe Projects::DiscussionsController do
expect(response).to have_gitlab_http_status(200)
end
+
+ context "when vue_mr_discussions cookie is present" do
+ before do
+ allow(controller).to receive(:cookies).and_return({ vue_mr_discussions: 'true' })
+ end
+
+ it "renders discussion with serializer" do
+ expect_any_instance_of(DiscussionSerializer).to receive(:represent)
+ .with(instance_of(Discussion), { context: instance_of(described_class) })
+
+ delete :unresolve, request_params
+ end
+ end
end
end
end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 9656e7f7e74..9918d52e402 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -974,7 +974,7 @@ describe Projects::IssuesController do
it 'returns discussion json' do
get :discussions, namespace_id: project.namespace, project_id: project, id: issue.iid
- expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes individual_note])
+ expect(json_response.first.keys).to match_array(%w[id reply_id expanded notes diff_discussion individual_note resolvable resolve_with_issue_path resolved])
end
context 'with cross-reference system note', :request_store do
diff --git a/spec/controllers/projects/pages_domains_controller_spec.rb b/spec/controllers/projects/pages_domains_controller_spec.rb
index 2192fd5cae2..83a3799e883 100644
--- a/spec/controllers/projects/pages_domains_controller_spec.rb
+++ b/spec/controllers/projects/pages_domains_controller_spec.rb
@@ -53,6 +53,66 @@ describe Projects::PagesDomainsController do
end
end
+ describe 'GET edit' do
+ it "displays the 'edit' page" do
+ get(:edit, request_params.merge(id: pages_domain.domain))
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to render_template('edit')
+ end
+ end
+
+ describe 'PATCH update' do
+ before do
+ controller.instance_variable_set(:@domain, pages_domain)
+ end
+
+ let(:pages_domain_params) do
+ attributes_for(:pages_domain, :with_certificate, :with_key).slice(:key, :certificate)
+ end
+
+ let(:params) do
+ request_params.merge(id: pages_domain.domain, pages_domain: pages_domain_params)
+ end
+
+ it 'updates the domain' do
+ expect(pages_domain)
+ .to receive(:update)
+ .with(pages_domain_params)
+ .and_return(true)
+
+ patch(:update, params)
+ end
+
+ it 'redirects to the project page' do
+ patch(:update, params)
+
+ expect(flash[:notice]).to eq 'Domain was updated'
+ expect(response).to redirect_to(project_pages_path(project))
+ end
+
+ context 'the domain is invalid' do
+ it 'renders the edit action' do
+ allow(pages_domain).to receive(:update).and_return(false)
+
+ patch(:update, params)
+
+ expect(response).to render_template('edit')
+ end
+ end
+
+ context 'the parameters include the domain' do
+ it 'renders 400 Bad Request' do
+ expect(pages_domain)
+ .to receive(:update)
+ .with(hash_not_including(:domain))
+ .and_return(true)
+
+ patch(:update, params.deep_merge(pages_domain: { domain: 'abc' }))
+ end
+ end
+ end
+
describe 'POST verify' do
let(:params) { request_params.merge(id: pages_domain.domain) }
diff --git a/spec/factories/badge.rb b/spec/factories/badge.rb
new file mode 100644
index 00000000000..b87ece946cb
--- /dev/null
+++ b/spec/factories/badge.rb
@@ -0,0 +1,14 @@
+FactoryBot.define do
+ trait :base_badge do
+ link_url { generate(:url) }
+ image_url { generate(:url) }
+ end
+
+ factory :project_badge, traits: [:base_badge], class: ProjectBadge do
+ project
+ end
+
+ factory :group_badge, aliases: [:badge], traits: [:base_badge], class: GroupBadge do
+ group
+ end
+end
diff --git a/spec/factories/clusters/applications/helm.rb b/spec/factories/clusters/applications/helm.rb
index 775fbb3d27b..3deca103578 100644
--- a/spec/factories/clusters/applications/helm.rb
+++ b/spec/factories/clusters/applications/helm.rb
@@ -34,5 +34,6 @@ FactoryBot.define do
factory :clusters_applications_ingress, class: Clusters::Applications::Ingress
factory :clusters_applications_prometheus, class: Clusters::Applications::Prometheus
+ factory :clusters_applications_runner, class: Clusters::Applications::Runner
end
end
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
index 8eb709022ce..caaed4d5246 100644
--- a/spec/factories/lfs_objects.rb
+++ b/spec/factories/lfs_objects.rb
@@ -9,4 +9,10 @@ FactoryBot.define do
trait :with_file do
file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
end
+
+ # The uniqueness constraint means we can't use the correct OID for all LFS
+ # objects, so the test needs to decide which (if any) object gets it
+ trait :correct_oid do
+ oid 'b804383982bb89b00e828e3f44c038cc991d3d1768009fc39ba8e2c081b9fb75'
+ end
end
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index a5f22848031..d5e603baeae 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -173,7 +173,7 @@ feature 'Admin Groups' do
visit admin_group_path(group)
- expect(page).to have_content(empty_project.name_with_namespace)
+ expect(page).to have_content(empty_project.full_name)
expect(page).to have_content('Projects shared with')
end
end
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index d02ac6c2e2a..6d8350e99f1 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -58,7 +58,7 @@ describe "Admin::Projects" do
expect(current_path).to eq admin_project_path(project)
expect(page).to have_content(project.path)
expect(page).to have_content(project.name)
- expect(page).to have_content(project.name_with_namespace)
+ expect(page).to have_content(project.full_name)
expect(page).to have_content(project.creator.name)
end
end
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 7eeed7da998..8de2e3d199b 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -76,8 +76,8 @@ describe "Admin Runners" do
describe 'projects' do
it 'contains project names' do
- expect(page).to have_content(@project1.name_with_namespace)
- expect(page).to have_content(@project2.name_with_namespace)
+ expect(page).to have_content(@project1.full_name)
+ expect(page).to have_content(@project2.full_name)
end
end
@@ -89,8 +89,8 @@ describe "Admin Runners" do
end
it 'contains name of correct project' do
- expect(page).to have_content(@project1.name_with_namespace)
- expect(page).not_to have_content(@project2.name_with_namespace)
+ expect(page).to have_content(@project1.full_name)
+ expect(page).not_to have_content(@project2.full_name)
end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 2307ba5985e..8f0a3611052 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -382,7 +382,7 @@ describe "Admin::Users" do
describe 'update user identities' do
before do
- allow(Gitlab::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:providers).and_return([:twitter, :twitter_updated])
end
it 'modifies twitter identity' do
diff --git a/spec/features/admin/services/admin_activates_prometheus_spec.rb b/spec/features/admin/services/admin_activates_prometheus_spec.rb
new file mode 100644
index 00000000000..904fe5b406b
--- /dev/null
+++ b/spec/features/admin/services/admin_activates_prometheus_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe 'Admin activates Prometheus' do
+ let(:admin) { create(:user, :admin) }
+
+ before do
+ sign_in(admin)
+
+ visit(admin_application_settings_services_path)
+
+ click_link('Prometheus')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('API URL', with: 'http://prometheus.example.com')
+ click_button('Save')
+
+ expect(page).to have_content('Application settings saved successfully')
+ end
+end
diff --git a/spec/features/cycle_analytics_spec.rb b/spec/features/cycle_analytics_spec.rb
index 510677ecf56..ef493db3f11 100644
--- a/spec/features/cycle_analytics_spec.rb
+++ b/spec/features/cycle_analytics_spec.rb
@@ -6,7 +6,7 @@ feature 'Cycle Analytics', :js do
let(:project) { create(:project, :repository) }
let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
let(:milestone) { create(:milestone, project: project) }
- let(:mr) { create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}") }
+ let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
context 'as an allowed user' do
@@ -41,8 +41,8 @@ feature 'Cycle Analytics', :js do
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
project.add_master(user)
- create_cycle
- deploy_master
+ @build = create_cycle(user, project, issue, mr, milestone, pipeline)
+ deploy_master(user, project)
sign_in(user)
visit project_cycle_analytics_path(project)
@@ -117,8 +117,8 @@ feature 'Cycle Analytics', :js do
project.add_guest(guest)
allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
- create_cycle
- deploy_master
+ create_cycle(user, project, issue, mr, milestone, pipeline)
+ deploy_master(user, project)
sign_in(guest)
visit project_cycle_analytics_path(project)
@@ -166,16 +166,6 @@ feature 'Cycle Analytics', :js do
expect(find('.stage-events')).to have_content("!#{mr.iid}")
end
- def create_cycle
- issue.update(milestone: milestone)
- pipeline.run
-
- @build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
-
- merge_merge_requests_closing_issue(issue)
- ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
- end
-
def click_stage(stage_name)
find('.stage-nav li', text: stage_name).click
wait_for_requests
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 54652e2d849..8d1d5a51750 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -74,8 +74,8 @@ RSpec.describe 'Dashboard Issues' do
find('.new-project-item-select-button').click
page.within('.select2-results') do
- expect(page).to have_content(project.name_with_namespace)
- expect(page).not_to have_content(project_with_issues_disabled.name_with_namespace)
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_issues_disabled.full_name)
end
end
@@ -84,8 +84,8 @@ RSpec.describe 'Dashboard Issues' do
wait_for_requests
- project_path = "/#{project.path_with_namespace}"
- project_json = { name: project.name_with_namespace, url: project_path }.to_json
+ project_path = "/#{project.full_path}"
+ project_json = { name: project.full_name, url: project_path }.to_json
# simulate selection, and prevent overlap by dropdown menu
first('.project-item-select', visible: false)
diff --git a/spec/features/dashboard/merge_requests_spec.rb b/spec/features/dashboard/merge_requests_spec.rb
index 744041ac425..c8f3a8449f5 100644
--- a/spec/features/dashboard/merge_requests_spec.rb
+++ b/spec/features/dashboard/merge_requests_spec.rb
@@ -28,8 +28,8 @@ feature 'Dashboard Merge Requests' do
find('.new-project-item-select-button').click
page.within('.select2-results') do
- expect(page).to have_content(project.name_with_namespace)
- expect(page).not_to have_content(project_with_disabled_merge_requests.name_with_namespace)
+ expect(page).to have_content(project.full_name)
+ expect(page).not_to have_content(project_with_disabled_merge_requests.full_name)
end
end
end
diff --git a/spec/features/dashboard/todos/todos_filtering_spec.rb b/spec/features/dashboard/todos/todos_filtering_spec.rb
index 2fc34301d51..7b359b0c651 100644
--- a/spec/features/dashboard/todos/todos_filtering_spec.rb
+++ b/spec/features/dashboard/todos/todos_filtering_spec.rb
@@ -24,14 +24,14 @@ feature 'Dashboard > User filters todos', :js do
it 'filters by project' do
click_button 'Project'
within '.dropdown-menu-project' do
- fill_in 'Search projects', with: project_1.name_with_namespace
- click_link project_1.name_with_namespace
+ fill_in 'Search projects', with: project_1.full_name
+ click_link project_1.full_name
end
wait_for_requests
- expect(page).to have_content project_1.name_with_namespace
- expect(page).not_to have_content project_2.name_with_namespace
+ expect(page).to have_content project_1.full_name
+ expect(page).not_to have_content project_2.full_name
end
context 'Author filter' do
diff --git a/spec/features/groups/empty_states_spec.rb b/spec/features/groups/empty_states_spec.rb
index 243e8536168..04217fec06c 100644
--- a/spec/features/groups/empty_states_spec.rb
+++ b/spec/features/groups/empty_states_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Groups Merge Requests Empty States' do
+feature 'Group empty states' do
let(:group) { create(:group) }
let(:user) { create(:group_member, :developer, user: create(:user), group: group ).user }
@@ -8,62 +8,100 @@ feature 'Groups Merge Requests Empty States' do
sign_in(user)
end
- context 'group has a project' do
- let(:project) { create(:project, namespace: group) }
+ [:issue, :merge_request].each do |issuable|
+ issuable_name = issuable.to_s.humanize.downcase
+ project_relation = issuable == :issue ? :project : :source_project
- before do
- project.add_master(user)
- end
+ context "for #{issuable_name}s" do
+ let(:path) { public_send(:"#{issuable}s_group_path", group) }
- context 'the project has a merge request' do
- before do
- create(:merge_request, source_project: project)
+ context 'group has a project' do
+ let(:project) { create(:project, namespace: group) }
- visit merge_requests_group_path(group)
- end
+ before do
+ project.add_master(user)
+ end
- it 'should not display an empty state' do
- expect(page).not_to have_selector('.empty-state')
- end
- end
+ context "the project has #{issuable_name}s" do
+ before do
+ create(issuable, project_relation => project)
- context 'the project has no merge requests', :js do
- before do
- visit merge_requests_group_path(group)
- end
+ visit path
+ end
- it 'should display an empty state' do
- expect(page).to have_selector('.empty-state')
- end
+ it 'does not display an empty state' do
+ expect(page).not_to have_selector('.empty-state')
+ end
+ end
+
+ context "the project has no #{issuable_name}s", :js do
+ before do
+ visit path
+ end
+
+ it 'displays an empty state' do
+ expect(page).to have_selector('.empty-state')
+ end
+
+ it "shows a new #{issuable_name} button" do
+ within '.empty-state' do
+ expect(page).to have_content("create #{issuable_name}")
+ end
+ end
+
+ it "the new #{issuable_name} button opens a project dropdown" do
+ within '.empty-state' do
+ find('.new-project-item-select-button').click
+ end
- it 'should show a new merge request button' do
- within '.empty-state' do
- expect(page).to have_content('create merge request')
+ expect(page).to have_selector('.ajax-project-dropdown')
+ end
end
end
- it 'the new merge request button opens a project dropdown' do
- within '.empty-state' do
- find('.new-project-item-select-button').click
- end
+ context 'group without a project' do
+ context 'group has a subgroup', :nested_groups do
+ let(:subgroup) { create(:group, parent: group) }
+ let(:subgroup_project) { create(:project, namespace: subgroup) }
- expect(page).to have_selector('.ajax-project-dropdown')
- end
- end
- end
+ context "the project has #{issuable_name}s" do
+ before do
+ create(issuable, project_relation => subgroup_project)
- context 'group without a project' do
- before do
- visit merge_requests_group_path(group)
- end
+ visit path
+ end
- it 'should display an empty state' do
- expect(page).to have_selector('.empty-state')
- end
+ it 'does not display an empty state' do
+ expect(page).not_to have_selector('.empty-state')
+ end
+ end
- it 'should not show a new merge request button' do
- within '.empty-state' do
- expect(page).not_to have_link('create merge request')
+ context "the project has no #{issuable_name}s" do
+ before do
+ visit path
+ end
+
+ it 'displays an empty state' do
+ expect(page).to have_selector('.empty-state')
+ end
+ end
+ end
+
+ context 'group has no subgroups' do
+ before do
+ visit path
+ end
+
+ it 'displays an empty state' do
+ expect(page).to have_selector('.empty-state')
+ end
+
+ it "shows a new #{issuable_name} button" do
+ within '.empty-state' do
+ expect(page).not_to have_link("create #{issuable_name}")
+ end
+ end
+ end
end
end
end
diff --git a/spec/features/groups/members/manage_members.rb b/spec/features/groups/members/manage_members_spec.rb
index 21f7b4999ad..21f7b4999ad 100644
--- a/spec/features/groups/members/manage_members.rb
+++ b/spec/features/groups/members/manage_members_spec.rb
diff --git a/spec/features/issues/filtered_search/recent_searches_spec.rb b/spec/features/issues/filtered_search/recent_searches_spec.rb
index f355cec3ba9..41b9ada988a 100644
--- a/spec/features/issues/filtered_search/recent_searches_spec.rb
+++ b/spec/features/issues/filtered_search/recent_searches_spec.rb
@@ -39,8 +39,8 @@ describe 'Recent searches', :js do
items = all('.filtered-search-history-dropdown-item', visible: false, count: 2)
- expect(items[0].text).to eq('label:~qux garply')
- expect(items[1].text).to eq('label:~foo bar')
+ expect(items[0].text).to eq('label: ~qux garply')
+ expect(items[1].text).to eq('label: ~foo bar')
end
it 'saved recent searches are restored last on the list' do
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
index faf14be4818..ef6b8edd0ad 100644
--- a/spec/features/issues/form_spec.rb
+++ b/spec/features/issues/form_spec.rb
@@ -189,6 +189,18 @@ describe 'New/edit issue', :js do
expect(find('.js-label-select')).to have_content('Labels')
end
+ it 'clears label search input field when a label is selected' do
+ click_button 'Labels'
+
+ page.within '.dropdown-menu-labels' do
+ search_field = find('input[type="search"]')
+
+ search_field.set(label2.title)
+ click_link label2.title
+ expect(search_field.value).to eq ''
+ end
+ end
+
it 'correctly updates the selected user when changing assignee' do
click_button 'Unassigned'
@@ -271,6 +283,18 @@ describe 'New/edit issue', :js do
end
end
+ context 'inline edit' do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'opens inline edit form with shortcut' do
+ find('body').send_keys('e')
+
+ expect(page).to have_selector('.detail-page-description form')
+ end
+ end
+
describe 'sub-group project' do
let(:group) { create(:group) }
let(:nested_group_1) { create(:group, parent: group) }
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 076a02150a4..3c01ff345fc 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -73,7 +73,7 @@ feature 'issue move to another project' do
wait_for_requests
page.within '.js-sidebar-move-issue-block' do
- expect(page).to have_content new_project.name_with_namespace
+ expect(page).to have_content new_project.full_name
end
end
end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index e711a191db2..ea7a97d02a0 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -59,7 +59,6 @@ feature 'Issues > User uses quick actions', :js do
it 'does not create a note, and sets the due date accordingly' do
write_note("/due 2016-08-28")
- expect(page).to have_content '/due 2016-08-28'
expect(page).not_to have_content 'Commands applied'
issue.reload
@@ -99,7 +98,6 @@ feature 'Issues > User uses quick actions', :js do
it 'does not create a note, and sets the due date accordingly' do
write_note("/remove_due_date")
- expect(page).to have_content '/remove_due_date'
expect(page).not_to have_content 'Commands applied'
issue.reload
@@ -147,7 +145,6 @@ feature 'Issues > User uses quick actions', :js do
it 'does not create a note, and does not mark the issue as a duplicate' do
write_note("/duplicate ##{original_issue.to_reference}")
- expect(page).to have_content "/duplicate ##{original_issue.to_reference}"
expect(page).not_to have_content 'Commands applied'
expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
diff --git a/spec/features/merge_request/user_posts_notes_spec.rb b/spec/features/merge_request/user_posts_notes_spec.rb
index 50d06565fc0..b54addce993 100644
--- a/spec/features/merge_request/user_posts_notes_spec.rb
+++ b/spec/features/merge_request/user_posts_notes_spec.rb
@@ -144,7 +144,7 @@ describe 'Merge request > User posts notes', :js do
end
end
- describe 'deleting an attachment' do
+ describe 'deleting attachment on legacy diff note' do
before do
find('.note').hover
diff --git a/spec/features/profiles/password_spec.rb b/spec/features/profiles/password_spec.rb
index 1d7700b6767..f9c6ff90ca1 100644
--- a/spec/features/profiles/password_spec.rb
+++ b/spec/features/profiles/password_spec.rb
@@ -134,5 +134,15 @@ describe 'Profile > Password' do
expect(current_path).to eq new_user_session_path
end
+
+ context 'when global require_two_factor_authentication is enabled' do
+ it 'needs change user password' do
+ stub_application_setting(require_two_factor_authentication: true)
+
+ visit profile_path
+
+ expect(current_path).to eq new_profile_password_path
+ end
+ end
end
end
diff --git a/spec/features/projects/branches/download_buttons_spec.rb b/spec/features/projects/branches/download_buttons_spec.rb
index 39bcea013e7..605298ba8ab 100644
--- a/spec/features/projects/branches/download_buttons_spec.rb
+++ b/spec/features/projects/branches/download_buttons_spec.rb
@@ -29,7 +29,7 @@ feature 'Download buttons in branches page' do
describe 'when checking branches' do
context 'with artifacts' do
before do
- visit project_branches_path(project, search: 'binary-encoding')
+ visit project_branches_filtered_path(project, state: 'all', search: 'binary-encoding')
end
scenario 'shows download artifacts button' do
diff --git a/spec/features/projects/branches_spec.rb b/spec/features/projects/branches_spec.rb
index 2fddd274078..2a9d9e6416c 100644
--- a/spec/features/projects/branches_spec.rb
+++ b/spec/features/projects/branches_spec.rb
@@ -11,15 +11,109 @@ describe 'Branches' do
project.add_developer(user)
end
- describe 'Initial branches page' do
- it 'shows all the branches sorted by last updated by default' do
+ context 'on the projects with 6 active branches and 4 stale branches' do
+ let(:project) { create(:project, :public, :empty_repo) }
+ let(:repository) { project.repository }
+ let(:threshold) { Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD }
+
+ before do
+ # Add 4 stale branches
+ (1..4).reverse_each do |i|
+ Timecop.freeze((threshold + i).ago) { create_file(message: "a commit in stale-#{i}", branch_name: "stale-#{i}") }
+ end
+ # Add 6 active branches
+ (1..6).each do |i|
+ Timecop.freeze((threshold - i).ago) { create_file(message: "a commit in active-#{i}", branch_name: "active-#{i}") }
+ end
+ end
+
+ describe 'Overview page of the branches' do
+ it 'shows the first 5 active branches and the first 4 stale branches sorted by last updated' do
+ visit project_branches_path(project)
+
+ expect(page).to have_content(sorted_branches(repository, count: 5, sort_by: :updated_desc, state: 'active'))
+ expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_desc, state: 'stale'))
+
+ expect(page).to have_link('Show more active branches', href: project_branches_filtered_path(project, state: 'active'))
+ expect(page).not_to have_content('Show more stale branches')
+ end
+ end
+
+ describe 'Active branches page' do
+ it 'shows 6 active branches sorted by last updated' do
+ visit project_branches_filtered_path(project, state: 'active')
+
+ expect(page).to have_content(sorted_branches(repository, count: 6, sort_by: :updated_desc, state: 'active'))
+ end
+ end
+
+ describe 'Stale branches page' do
+ it 'shows 4 active branches sorted by last updated' do
+ visit project_branches_filtered_path(project, state: 'stale')
+
+ expect(page).to have_content(sorted_branches(repository, count: 4, sort_by: :updated_desc, state: 'stale'))
+ end
+ end
+
+ describe 'All branches page' do
+ it 'shows 10 branches sorted by last updated' do
+ visit project_branches_filtered_path(project, state: 'all')
+
+ expect(page).to have_content(sorted_branches(repository, count: 10, sort_by: :updated_desc))
+ end
+ end
+
+ context 'with branches over more than one page' do
+ before do
+ allow(Kaminari.config).to receive(:default_per_page).and_return(5)
+ end
+
+ it 'shows only default_per_page active branches sorted by last updated' do
+ visit project_branches_filtered_path(project, state: 'active')
+
+ expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page, sort_by: :updated_desc, state: 'active'))
+ end
+
+ it 'shows only default_per_page branches sorted by last updated on All branches' do
+ visit project_branches_filtered_path(project, state: 'all')
+
+ expect(page).to have_content(sorted_branches(repository, count: Kaminari.config.default_per_page, sort_by: :updated_desc))
+ end
+ end
+ end
+
+ describe 'Find branches' do
+ it 'shows filtered branches', :js do
visit project_branches_path(project)
+ fill_in 'branch-search', with: 'fix'
+ find('#branch-search').native.send_keys(:enter)
+
+ expect(page).to have_content('fix')
+ expect(find('.all-branches')).to have_selector('li', count: 1)
+ end
+ end
+
+ describe 'Delete unprotected branch on Overview' do
+ it 'removes branch after confirmation', :js do
+ visit project_branches_filtered_path(project, state: 'all')
+
+ expect(all('.all-branches').last).to have_selector('li', count: 20)
+ accept_confirm { find('.js-branch-add-pdf-text-binary .btn-remove').click }
+
+ expect(all('.all-branches').last).to have_selector('li', count: 19)
+ end
+ end
+
+ describe 'All branches page' do
+ it 'shows all the branches sorted by last updated by default' do
+ visit project_branches_filtered_path(project, state: 'all')
+
expect(page).to have_content(sorted_branches(repository, count: 20, sort_by: :updated_desc))
end
it 'sorts the branches by name' do
- visit project_branches_path(project)
+ visit project_branches_filtered_path(project, state: 'all')
click_button "Last updated" # Open sorting dropdown
click_link "Name"
@@ -28,7 +122,7 @@ describe 'Branches' do
end
it 'sorts the branches by oldest updated' do
- visit project_branches_path(project)
+ visit project_branches_filtered_path(project, state: 'all')
click_button "Last updated" # Open sorting dropdown
click_link "Oldest updated"
@@ -41,13 +135,13 @@ describe 'Branches' do
%w(one two three four five).each { |ref| repository.add_branch(user, ref, 'master') }
- expect { visit project_branches_path(project) }.not_to exceed_query_limit(control_count)
+ expect { visit project_branches_filtered_path(project, state: 'all') }.not_to exceed_query_limit(control_count)
end
end
- describe 'Find branches' do
+ describe 'Find branches on All branches' do
it 'shows filtered branches', :js do
- visit project_branches_path(project)
+ visit project_branches_filtered_path(project, state: 'all')
fill_in 'branch-search', with: 'fix'
find('#branch-search').native.send_keys(:enter)
@@ -57,9 +151,9 @@ describe 'Branches' do
end
end
- describe 'Delete unprotected branch' do
+ describe 'Delete unprotected branch on All branches' do
it 'removes branch after confirmation', :js do
- visit project_branches_path(project)
+ visit project_branches_filtered_path(project, state: 'all')
fill_in 'branch-search', with: 'fix'
@@ -73,6 +167,19 @@ describe 'Branches' do
expect(find('.all-branches')).to have_selector('li', count: 0)
end
end
+
+ context 'on project with 0 branch' do
+ let(:project) { create(:project, :public, :empty_repo) }
+ let(:repository) { project.repository }
+
+ describe '0 branches on Overview' do
+ it 'shows warning' do
+ visit project_branches_path(project)
+
+ expect(page).not_to have_selector('.all-branches')
+ end
+ end
+ end
end
context 'logged in as master' do
@@ -83,7 +190,7 @@ describe 'Branches' do
describe 'Initial branches page' do
it 'shows description for admin' do
- visit project_branches_path(project)
+ visit project_branches_filtered_path(project, state: 'all')
expect(page).to have_content("Protected branches can be managed in project settings")
end
@@ -102,12 +209,18 @@ describe 'Branches' do
end
end
- def sorted_branches(repository, count:, sort_by:)
+ def sorted_branches(repository, count:, sort_by:, state: nil)
+ branches = repository.branches_sorted_by(sort_by)
+ branches = branches.select { |b| state == 'active' ? b.active? : b.stale? } if state
sorted_branches =
- repository.branches_sorted_by(sort_by).first(count).map do |branch|
+ branches.first(count).map do |branch|
Regexp.escape(branch.name)
end
Regexp.new(sorted_branches.join('.*'))
end
+
+ def create_file(message: 'message', branch_name:)
+ repository.create_file(user, generate(:branch), 'content', message: message, branch_name: branch_name)
+ end
end
diff --git a/spec/features/projects/clusters/applications_spec.rb b/spec/features/projects/clusters/applications_spec.rb
index 8d1e10b7191..7b2c57aa652 100644
--- a/spec/features/projects/clusters/applications_spec.rb
+++ b/spec/features/projects/clusters/applications_spec.rb
@@ -22,7 +22,7 @@ feature 'Clusters Applications', :js do
scenario 'user is unable to install applications' do
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button').text).to eq('Install')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end
end
end
@@ -33,13 +33,13 @@ feature 'Clusters Applications', :js do
scenario 'user can install applications' do
page.within('.js-cluster-application-row-helm') do
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to be_nil
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
end
end
context 'when user installs Helm' do
before do
- allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
page.within('.js-cluster-application-row-helm') do
page.find(:css, '.js-cluster-application-install-button').click
@@ -50,18 +50,18 @@ feature 'Clusters Applications', :js do
page.within('.js-cluster-application-row-helm') do
# FE sends request and gets the response, then the buttons is "Install"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_helm.make_installing!
# FE starts polling and update the buttons to "Installing"
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
Clusters::Cluster.last.application_helm.make_installed!
expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
end
expect(page).to have_content('Helm Tiller was successfully installed on your Kubernetes cluster')
@@ -71,11 +71,14 @@ feature 'Clusters Applications', :js do
context 'when user installs Ingress' do
context 'when user installs application: Ingress' do
before do
- allow(ClusterInstallAppWorker).to receive(:perform_async).and_return(nil)
+ allow(ClusterInstallAppWorker).to receive(:perform_async)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
create(:clusters_applications_helm, :installed, cluster: cluster)
page.within('.js-cluster-application-row-ingress') do
+ expect(page).to have_css('.js-cluster-application-install-button:not([disabled])')
page.find(:css, '.js-cluster-application-install-button').click
end
end
@@ -83,19 +86,28 @@ feature 'Clusters Applications', :js do
it 'he sees status transition' do
page.within('.js-cluster-application-row-ingress') do
# FE sends request and gets the response, then the buttons is "Install"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Install')
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Install')
Clusters::Cluster.last.application_ingress.make_installing!
# FE starts polling and update the buttons to "Installing"
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installing')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installing')
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]')
+ # The application becomes installed but we keep waiting for external IP address
Clusters::Cluster.last.application_ingress.make_installed!
- expect(page.find(:css, '.js-cluster-application-install-button')['disabled']).to eq('true')
- expect(page.find(:css, '.js-cluster-application-install-button')).to have_content('Installed')
+ expect(page).to have_css('.js-cluster-application-install-button', exact_text: 'Installed')
+ expect(page).to have_css('.js-cluster-application-install-button[disabled]')
+ expect(page).to have_selector('.js-no-ip-message')
+ expect(page.find('.js-ip-address').value).to eq('?')
+
+ # We receive the external IP address and display
+ Clusters::Cluster.last.application_ingress.update!(external_ip: '192.168.1.100')
+
+ expect(page).not_to have_selector('.js-no-ip-message')
+ expect(page.find('.js-ip-address').value).to eq('192.168.1.100')
end
expect(page).to have_content('Ingress was successfully installed on your Kubernetes cluster')
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index 64e600144e0..b233af83eec 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -234,7 +234,7 @@ feature 'Environment' do
end
scenario 'user deletes the branch with running environment' do
- visit project_branches_path(project, search: 'feature')
+ visit project_branches_filtered_path(project, state: 'all', search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
page.within('.js-branch-feature') { find('a.btn-remove').click }
diff --git a/spec/features/projects/import_export/test_project_export.tar.gz b/spec/features/projects/import_export/test_project_export.tar.gz
index 0cc68aff494..12bfcc177c7 100644
--- a/spec/features/projects/import_export/test_project_export.tar.gz
+++ b/spec/features/projects/import_export/test_project_export.tar.gz
Binary files differ
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index d575596937d..1f4eec0a317 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -25,7 +25,7 @@ feature 'Projects > Members > Master manages access requests' do
perform_enqueued_jobs { click_on 'Grant access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was granted"
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was granted"
end
scenario 'master can deny access' do
@@ -36,7 +36,7 @@ feature 'Projects > Members > Master manages access requests' do
perform_enqueued_jobs { click_on 'Deny access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [user.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.name_with_namespace} project was denied"
+ expect(ActionMailer::Base.deliveries.last.subject).to match "Access to the #{project.full_name} project was denied"
end
def expect_visible_access_request(project, user)
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index 4eb36156812..672d5daa3d8 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -21,7 +21,7 @@ feature 'Projects > Members > User requests access', :js do
perform_enqueued_jobs { click_link 'Request Access' }
expect(ActionMailer::Base.deliveries.last.to).to eq [master.notification_email]
- expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.name_with_namespace} project"
+ expect(ActionMailer::Base.deliveries.last.subject).to eq "Request to join the #{project.full_name} project"
expect(project.requesters.exists?(user_id: user)).to be_truthy
expect(page).to have_content 'Your request for access has been queued for review.'
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 85d518c0cc3..40689964b91 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -81,8 +81,8 @@ feature 'Merge Request button' do
context 'on branches page' do
it_behaves_like 'Merge request button only shown when allowed' do
let(:label) { 'Merge request' }
- let(:url) { project_branches_path(project, search: 'feature') }
- let(:fork_url) { project_branches_path(forked_project, search: 'feature') }
+ let(:url) { project_branches_filtered_path(project, state: 'all', search: 'feature') }
+ let(:fork_url) { project_branches_filtered_path(forked_project, state: 'all', search: 'feature') }
end
end
diff --git a/spec/features/projects/new_project_spec.rb b/spec/features/projects/new_project_spec.rb
index b5104747d00..fd561288091 100644
--- a/spec/features/projects/new_project_spec.rb
+++ b/spec/features/projects/new_project_spec.rb
@@ -142,7 +142,7 @@ feature 'New project' do
context 'from git repository url, "Repo by URL"' do
before do
- first('.import_git').click
+ first('.js-import-git-toggle-button').click
end
it 'does not autocomplete sensitive git repo URL' do
diff --git a/spec/features/projects/pages_spec.rb b/spec/features/projects/pages_spec.rb
index a96f2c186a4..233d2e67b9d 100644
--- a/spec/features/projects/pages_spec.rb
+++ b/spec/features/projects/pages_spec.rb
@@ -160,6 +160,37 @@ feature 'Pages' do
expect(page).to have_content('my.test.domain.com')
end
+
+ describe 'updating the certificate for an existing domain' do
+ let!(:domain) do
+ create(:pages_domain, :with_key, :with_certificate, project: project)
+ end
+
+ it 'allows the certificate to be updated' do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Details' }
+ click_link 'Edit'
+ click_button 'Save Changes'
+
+ expect(page).to have_content('Domain was updated')
+ end
+
+ context 'when the certificate is invalid' do
+ it 'tells the user what the problem is' do
+ visit project_pages_path(project)
+
+ within('#content-body') { click_link 'Details' }
+ click_link 'Edit'
+ fill_in 'Certificate (PEM)', with: 'invalid data'
+ click_button 'Save Changes'
+
+ expect(page).to have_content('Certificate must be a valid PEM certificate')
+ expect(page).to have_content('Certificate misses intermediates')
+ expect(page).to have_content("Key doesn't match the certificate")
+ end
+ end
+ end
end
end
@@ -227,7 +258,7 @@ feature 'Pages' do
end
let(:ci_build) do
- build(
+ create(
:ci_build,
project: project,
pipeline: pipeline,
diff --git a/spec/features/projects/pipelines/pipelines_spec.rb b/spec/features/projects/pipelines/pipelines_spec.rb
index 37a06b65481..849d85061df 100644
--- a/spec/features/projects/pipelines/pipelines_spec.rb
+++ b/spec/features/projects/pipelines/pipelines_spec.rb
@@ -86,7 +86,22 @@ describe 'Pipelines', :js do
it 'updates content when tab is clicked' do
page.find('.js-pipelines-tab-pending').click
wait_for_requests
- expect(page).to have_content('No pipelines to show.')
+ expect(page).to have_content('There are currently no pending pipelines.')
+ end
+ end
+
+ context 'navigation links' do
+ before do
+ visit project_pipelines_path(project)
+ wait_for_requests
+ end
+
+ it 'renders run pipeline link' do
+ expect(page).to have_link('Run Pipeline')
+ end
+
+ it 'renders ci lint link' do
+ expect(page).to have_link('CI Lint')
end
end
@@ -367,23 +382,6 @@ describe 'Pipelines', :js do
expect(build.reload).to be_canceled
end
end
-
- context 'dropdown jobs list' do
- it 'should keep the dropdown open when the user ctr/cmd + clicks in the job name' do
- find('.js-builds-dropdown-button').click
- dropdown_item = find('.mini-pipeline-graph-dropdown-item').native
-
- %i(alt control).each do |meta_key|
- page.driver.browser.action
- .key_down(meta_key)
- .click(dropdown_item)
- .key_up(meta_key)
- .perform
- end
-
- expect(page).to have_selector('.js-ci-action-icon')
- end
- end
end
context 'with pagination' do
@@ -559,7 +557,7 @@ describe 'Pipelines', :js do
end
it 'has a clear caches button' do
- expect(page).to have_link 'Clear runner caches'
+ expect(page).to have_link 'Clear Runner Caches'
end
describe 'user clicks the button' do
@@ -569,19 +567,31 @@ describe 'Pipelines', :js do
end
it 'increments jobs_cache_index' do
- click_link 'Clear runner caches'
+ click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end
end
context 'when project does not have jobs_cache_index' do
it 'sets jobs_cache_index to 1' do
- click_link 'Clear runner caches'
+ click_link 'Clear Runner Caches'
expect(page.find('.flash-notice')).to have_content 'Project cache successfully reset.'
end
end
end
end
+
+ describe 'Empty State' do
+ let(:project) { create(:project, :repository) }
+
+ before do
+ visit project_pipelines_path(project)
+ end
+
+ it 'renders empty state' do
+ expect(page).to have_content 'Build with confidence'
+ end
+ end
end
context 'when user is not logged in' do
@@ -592,7 +602,9 @@ describe 'Pipelines', :js do
context 'when project is public' do
let(:project) { create(:project, :public, :repository) }
- it { expect(page).to have_content 'Build with confidence' }
+ context 'without pipelines' do
+ it { expect(page).to have_content 'This project is not currently set up to run pipelines.' }
+ end
end
context 'when project is private' do
diff --git a/spec/features/projects/services/user_activates_prometheus_spec.rb b/spec/features/projects/services/user_activates_prometheus_spec.rb
new file mode 100644
index 00000000000..33f884eb148
--- /dev/null
+++ b/spec/features/projects/services/user_activates_prometheus_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe 'User activates Prometheus' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.add_master(user)
+ sign_in(user)
+
+ visit(project_settings_integrations_path(project))
+
+ click_link('Prometheus')
+ end
+
+ it 'activates service' do
+ check('Active')
+ fill_in('API URL', with: 'http://prometheus.example.com')
+ click_button('Save changes')
+
+ expect(page).to have_content('Prometheus activated.')
+ end
+end
diff --git a/spec/features/projects/settings/user_manages_project_members_spec.rb b/spec/features/projects/settings/user_manages_project_members_spec.rb
index 2709047b8de..0a4f57bcd21 100644
--- a/spec/features/projects/settings/user_manages_project_members_spec.rb
+++ b/spec/features/projects/settings/user_manages_project_members_spec.rb
@@ -39,7 +39,7 @@ describe 'User manages project members' do
click_link('Import')
end
- select(project2.name_with_namespace, from: 'source_project_id')
+ select(project2.full_name, from: 'source_project_id')
click_button('Import')
project_member = project.project_members.find_by(user_id: user_mike.id)
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
deleted file mode 100644
index 0c67196f53e..00000000000
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ /dev/null
@@ -1,57 +0,0 @@
-require 'spec_helper'
-
-feature 'Multi-file editor new directory', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- before do
- project.add_master(user)
- sign_in(user)
-
- set_cookie('new_repo', 'true')
-
- visit project_tree_path(project, :master)
-
- wait_for_requests
-
- click_link('Web IDE')
-
- wait_for_requests
- end
-
- after do
- set_cookie('new_repo', 'false')
- end
-
- it 'creates directory in current directory' do
- find('.add-to-tree').click
-
- click_link('New directory')
-
- page.within('.modal') do
- 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 ide')
-
- click_button('Commit')
-
- 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
deleted file mode 100644
index 85f7318c05d..00000000000
--- a/spec/features/projects/tree/create_file_spec.rb
+++ /dev/null
@@ -1,47 +0,0 @@
-require 'spec_helper'
-
-feature 'Multi-file editor new file', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
-
- before do
- project.add_master(user)
- sign_in(user)
-
- set_cookie('new_repo', 'true')
-
- visit project_tree_path(project, :master)
-
- wait_for_requests
-
- click_link('Web IDE')
-
- wait_for_requests
- end
-
- after do
- set_cookie('new_repo', 'false')
- end
-
- it 'creates file in current directory' do
- find('.add-to-tree').click
-
- click_link('New file')
-
- page.within('.modal') 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 ide')
-
- click_button('Commit')
-
- 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
deleted file mode 100644
index f81e8677e92..00000000000
--- a/spec/features/projects/tree/upload_file_spec.rb
+++ /dev/null
@@ -1,53 +0,0 @@
-require 'spec_helper'
-
-feature 'Multi-file editor upload file', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:txt_file) { File.join(Rails.root, 'spec', 'fixtures', 'doc_sample.txt') }
- let(:img_file) { File.join(Rails.root, 'spec', 'fixtures', 'dk.png') }
-
- before do
- project.add_master(user)
- sign_in(user)
-
- set_cookie('new_repo', 'true')
-
- visit project_tree_path(project, :master)
-
- wait_for_requests
-
- click_link('Web IDE')
-
- wait_for_requests
- end
-
- after do
- set_cookie('new_repo', 'false')
- end
-
- it 'uploads text file' do
- find('.add-to-tree').click
-
- # make the field visible so capybara can use it
- execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
- attach_file('file-upload', txt_file)
-
- find('.add-to-tree').click
-
- expect(page).to have_selector('.multi-file-tab', text: 'doc_sample.txt')
- expect(find('.blob-editor-container .lines-content')['innerText']).to have_content(File.open(txt_file, &:readline))
- end
-
- it 'uploads image file' do
- find('.add-to-tree').click
-
- # make the field visible so capybara can use it
- execute_script('document.querySelector("#file-upload").classList.remove("hidden")')
- attach_file('file-upload', img_file)
-
- find('.add-to-tree').click
-
- expect(page).to have_selector('.multi-file-tab', text: 'dk.png')
- expect(page).not_to have_selector('.monaco-editor')
- end
-end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 645d12da09f..cfe979a8647 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -146,8 +146,8 @@ feature 'Project' do
end
describe 'removal', :js do
- let(:user) { create(:user, username: 'test', name: 'test') }
- let(:project) { create(:project, namespace: user.namespace, name: 'project1') }
+ let(:user) { create(:user) }
+ let(:project) { create(:project, namespace: user.namespace) }
before do
sign_in(user)
@@ -156,8 +156,8 @@ feature 'Project' do
end
it 'removes a project' do
- expect { remove_with_confirm('Remove project', project.path) }.to change {Project.count}.by(-1)
- expect(page).to have_content "Project 'test / project1' is in the process of being deleted."
+ expect { remove_with_confirm('Remove project', project.path) }.to change { Project.count }.by(-1)
+ expect(page).to have_content "Project '#{project.full_name}' is in the process of being deleted."
expect(Project.all.count).to be_zero
expect(project.issues).to be_empty
expect(project.merge_requests).to be_empty
diff --git a/spec/features/search/user_searches_for_code_spec.rb b/spec/features/search/user_searches_for_code_spec.rb
index 77212fb105b..9e089c5a6cb 100644
--- a/spec/features/search/user_searches_for_code_spec.rb
+++ b/spec/features/search/user_searches_for_code_spec.rb
@@ -35,7 +35,7 @@ describe 'User searches for code' do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: 'rspec')
diff --git a/spec/features/search/user_searches_for_issues_spec.rb b/spec/features/search/user_searches_for_issues_spec.rb
index ef9553f2a91..d6120ff8517 100644
--- a/spec/features/search/user_searches_for_issues_spec.rb
+++ b/spec/features/search/user_searches_for_issues_spec.rb
@@ -34,7 +34,7 @@ describe 'User searches for issues', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: issue1.title)
diff --git a/spec/features/search/user_searches_for_merge_requests_spec.rb b/spec/features/search/user_searches_for_merge_requests_spec.rb
index 3b6739aecbd..68e2f7a857d 100644
--- a/spec/features/search/user_searches_for_merge_requests_spec.rb
+++ b/spec/features/search/user_searches_for_merge_requests_spec.rb
@@ -33,7 +33,7 @@ describe 'User searches for merge requests', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: merge_request1.title)
diff --git a/spec/features/search/user_searches_for_milestones_spec.rb b/spec/features/search/user_searches_for_milestones_spec.rb
index 6e197aee498..fc6cd81eb68 100644
--- a/spec/features/search/user_searches_for_milestones_spec.rb
+++ b/spec/features/search/user_searches_for_milestones_spec.rb
@@ -33,7 +33,7 @@ describe 'User searches for milestones', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: milestone1.title)
diff --git a/spec/features/search/user_searches_for_wiki_pages_spec.rb b/spec/features/search/user_searches_for_wiki_pages_spec.rb
index 00af625dc86..7934779058f 100644
--- a/spec/features/search/user_searches_for_wiki_pages_spec.rb
+++ b/spec/features/search/user_searches_for_wiki_pages_spec.rb
@@ -18,7 +18,7 @@ describe 'User searches for wiki pages', :js do
find('.js-search-project-dropdown').click
page.within('.project-filter') do
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
fill_in('dashboard_search', with: 'content')
diff --git a/spec/features/search/user_uses_search_filters_spec.rb b/spec/features/search/user_uses_search_filters_spec.rb
index aa883c964d2..66afe163447 100644
--- a/spec/features/search/user_uses_search_filters_spec.rb
+++ b/spec/features/search/user_uses_search_filters_spec.rb
@@ -31,7 +31,7 @@ describe 'User uses search filters', :js do
wait_for_requests
- expect(page).to have_link(group_project.name_with_namespace)
+ expect(page).to have_link(group_project.full_name)
end
end
end
@@ -43,10 +43,10 @@ describe 'User uses search filters', :js do
wait_for_requests
- click_link(project.name_with_namespace)
+ click_link(project.full_name)
end
- expect(find('.js-search-project-dropdown')).to have_content(project.name_with_namespace)
+ expect(find('.js-search-project-dropdown')).to have_content(project.full_name)
end
end
end
diff --git a/spec/features/u2f_spec.rb b/spec/features/u2f_spec.rb
index 50ee1656e10..fb65b570dd6 100644
--- a/spec/features/u2f_spec.rb
+++ b/spec/features/u2f_spec.rb
@@ -1,10 +1,6 @@
require 'spec_helper'
feature 'Using U2F (Universal 2nd Factor) Devices for Authentication', :js do
- before do
- allow_any_instance_of(U2fHelper).to receive(:inject_u2f_api?).and_return(true)
- end
-
def manage_two_factor_authentication
click_on 'Manage two-factor authentication'
expect(page).to have_content("Setup new U2F device")
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index abb7631d7d7..45439640ea3 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -10,9 +10,9 @@ describe IssuesFinder do
set(:project3) { create(:project, group: subgroup) }
set(:milestone) { create(:milestone, project: project1) }
set(:label) { create(:label, project: project2) }
- set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago) }
- set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab') }
- set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 1.week.from_now) }
+ set(:issue1) { create(:issue, author: user, assignees: [user], project: project1, milestone: milestone, title: 'gitlab', created_at: 1.week.ago, updated_at: 1.week.ago) }
+ set(:issue2) { create(:issue, author: user, assignees: [user], project: project2, description: 'gitlab', created_at: 1.week.from_now, updated_at: 1.week.from_now) }
+ set(:issue3) { create(:issue, author: user2, assignees: [user2], project: project2, title: 'tanuki', description: 'tanuki', created_at: 2.weeks.from_now, updated_at: 2.weeks.from_now) }
set(:issue4) { create(:issue, project: project3) }
set(:award_emoji1) { create(:award_emoji, name: 'thumbsup', user: user, awardable: issue1) }
set(:award_emoji2) { create(:award_emoji, name: 'thumbsup', user: user2, awardable: issue2) }
@@ -275,12 +275,46 @@ describe IssuesFinder do
end
context 'through created_before' do
- let(:params) { { created_before: issue1.created_at + 1.second } }
+ let(:params) { { created_before: issue1.created_at } }
it 'returns issues created on or before the given date' do
expect(issues).to contain_exactly(issue1)
end
end
+
+ context 'through created_after and created_before' do
+ let(:params) { { created_after: issue2.created_at, created_before: issue3.created_at } }
+
+ it 'returns issues created between the given dates' do
+ expect(issues).to contain_exactly(issue2, issue3)
+ end
+ end
+ end
+
+ context 'filtering by updated_at' do
+ context 'through updated_after' do
+ let(:params) { { updated_after: issue3.updated_at } }
+
+ it 'returns issues updated on or after the given date' do
+ expect(issues).to contain_exactly(issue3)
+ end
+ end
+
+ context 'through updated_before' do
+ let(:params) { { updated_before: issue1.updated_at } }
+
+ it 'returns issues updated on or before the given date' do
+ expect(issues).to contain_exactly(issue1)
+ end
+ end
+
+ context 'through updated_after and updated_before' do
+ let(:params) { { updated_after: issue2.updated_at, updated_before: issue3.updated_at } }
+
+ it 'returns issues updated between the given dates' do
+ expect(issues).to contain_exactly(issue2, issue3)
+ end
+ end
end
context 'filtering by reaction name' do
diff --git a/spec/finders/labels_finder_spec.rb b/spec/finders/labels_finder_spec.rb
index 06031aee217..d434c501110 100644
--- a/spec/finders/labels_finder_spec.rb
+++ b/spec/finders/labels_finder_spec.rb
@@ -5,6 +5,8 @@ describe LabelsFinder do
let(:group_1) { create(:group) }
let(:group_2) { create(:group) }
let(:group_3) { create(:group) }
+ let(:private_group_1) { create(:group, :private) }
+ let(:private_subgroup_1) { create(:group, :private, parent: private_group_1) }
let(:project_1) { create(:project, namespace: group_1) }
let(:project_2) { create(:project, namespace: group_2) }
@@ -20,6 +22,8 @@ describe LabelsFinder do
let!(:group_label_1) { create(:group_label, group: group_1, title: 'Label 1 (group)') }
let!(:group_label_2) { create(:group_label, group: group_1, title: 'Group Label 2') }
let!(:group_label_3) { create(:group_label, group: group_2, title: 'Group Label 3') }
+ let!(:private_group_label_1) { create(:group_label, group: private_group_1, title: 'Private Group Label 1') }
+ let!(:private_subgroup_label_1) { create(:group_label, group: private_subgroup_1, title: 'Private Sub Group Label 1') }
let(:user) { create(:user) }
@@ -66,6 +70,44 @@ describe LabelsFinder do
expect(finder.execute).to eq [group_label_2, group_label_1]
end
end
+
+ context 'when including labels from group ancestors', :nested_groups do
+ it 'returns labels from group and its ancestors' do
+ private_group_1.add_developer(user)
+ private_subgroup_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: private_subgroup_1.id, only_group_labels: true, include_ancestor_groups: true)
+
+ expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1]
+ end
+
+ it 'ignores labels from groups which user can not read' do
+ private_subgroup_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: private_subgroup_1.id, only_group_labels: true, include_ancestor_groups: true)
+
+ expect(finder.execute).to eq [private_subgroup_label_1]
+ end
+ end
+
+ context 'when including labels from group descendants', :nested_groups do
+ it 'returns labels from group and its descendants' do
+ private_group_1.add_developer(user)
+ private_subgroup_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true)
+
+ expect(finder.execute).to eq [private_group_label_1, private_subgroup_label_1]
+ end
+
+ it 'ignores labels from groups which user can not read' do
+ private_subgroup_1.add_developer(user)
+
+ finder = described_class.new(user, group_id: private_group_1.id, only_group_labels: true, include_descendant_groups: true)
+
+ expect(finder.execute).to eq [private_subgroup_label_1]
+ end
+ end
end
context 'filtering by project_id' do
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 9385c892c9e..c8a43ddf410 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -18,7 +18,7 @@ describe MergeRequestsFinder do
let(:project4) { create(:project, :public, group: subgroup) }
let!(:merge_request1) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1) }
- let!(:merge_request2) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project1, state: 'closed') }
+ let!(:merge_request2) { create(:merge_request, :conflict, author: user, source_project: project2, target_project: project1, state: 'closed') }
let!(:merge_request3) { create(:merge_request, :simple, author: user, source_project: project2, target_project: project2) }
let!(:merge_request4) { create(:merge_request, :simple, author: user, source_project: project3, target_project: project3) }
let!(:merge_request5) { create(:merge_request, :simple, author: user, source_project: project4, target_project: project4) }
@@ -74,6 +74,22 @@ describe MergeRequestsFinder do
expect(merge_requests).to contain_exactly(merge_request1)
end
+ it 'filters by source branch' do
+ params = { source_branch: merge_request2.source_branch }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request2)
+ end
+
+ it 'filters by target branch' do
+ params = { target_branch: merge_request2.target_branch }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(merge_request2)
+ end
+
context 'filtering by group milestone' do
let!(:group) { create(:group, :public) }
let(:group_milestone) { create(:milestone, group: group) }
@@ -93,7 +109,7 @@ describe MergeRequestsFinder do
end
end
- context 'with created_after and created_before params' do
+ context 'filtering by created_at/updated_at' do
let(:new_project) { create(:project, forked_from_project: project1) }
let!(:new_merge_request) do
@@ -101,15 +117,18 @@ describe MergeRequestsFinder do
:simple,
author: user,
created_at: 1.week.from_now,
+ updated_at: 1.week.from_now,
source_project: new_project,
- target_project: project1)
+ target_project: new_project)
end
let!(:old_merge_request) do
create(:merge_request,
:simple,
author: user,
+ source_branch: 'feature_1',
created_at: 1.week.ago,
+ updated_at: 1.week.ago,
source_project: new_project,
target_project: new_project)
end
@@ -119,7 +138,7 @@ describe MergeRequestsFinder do
end
it 'filters by created_after' do
- params = { project_id: project1.id, created_after: new_merge_request.created_at }
+ params = { project_id: new_project.id, created_after: new_merge_request.created_at }
merge_requests = described_class.new(user, params).execute
@@ -127,12 +146,52 @@ describe MergeRequestsFinder do
end
it 'filters by created_before' do
- params = { project_id: new_project.id, created_before: old_merge_request.created_at + 1.second }
+ params = { project_id: new_project.id, created_before: old_merge_request.created_at }
merge_requests = described_class.new(user, params).execute
expect(merge_requests).to contain_exactly(old_merge_request)
end
+
+ it 'filters by created_after and created_before' do
+ params = {
+ project_id: new_project.id,
+ created_after: old_merge_request.created_at,
+ created_before: new_merge_request.created_at
+ }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
+ end
+
+ it 'filters by updated_after' do
+ params = { project_id: new_project.id, updated_after: new_merge_request.updated_at }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(new_merge_request)
+ end
+
+ it 'filters by updated_before' do
+ params = { project_id: new_project.id, updated_before: old_merge_request.updated_at }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request)
+ end
+
+ it 'filters by updated_after and updated_before' do
+ params = {
+ project_id: new_project.id,
+ updated_after: old_merge_request.updated_at,
+ updated_before: new_merge_request.updated_at
+ }
+
+ merge_requests = described_class.new(user, params).execute
+
+ expect(merge_requests).to contain_exactly(old_merge_request, new_merge_request)
+ end
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 7b43494eea2..f1ae2c7ab65 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -75,6 +75,18 @@ describe NotesFinder do
end
end
+ context 'for target type' do
+ let(:project) { create(:project, :repository) }
+ let!(:note1) { create :note_on_issue, project: project }
+ let!(:note2) { create :note_on_commit, project: project }
+
+ it 'finds only notes for the selected type' do
+ notes = described_class.new(project, user, target_type: 'issue').execute
+
+ expect(notes).to eq([note1])
+ end
+ end
+
context 'for target' do
let(:project) { create(:project, :repository) }
let(:note1) { create :note_on_commit, project: project }
diff --git a/spec/finders/todos_finder_spec.rb b/spec/finders/todos_finder_spec.rb
index 90eb0fe21e4..9747b9402a7 100644
--- a/spec/finders/todos_finder_spec.rb
+++ b/spec/finders/todos_finder_spec.rb
@@ -2,12 +2,13 @@ require 'spec_helper'
describe TodosFinder do
describe '#execute' do
- let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:finder) { described_class }
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+ let(:finder) { described_class }
before do
- project.add_developer(user)
+ group.add_developer(user)
end
describe '#sort' do
@@ -34,17 +35,20 @@ describe TodosFinder do
end
it "sorts by priority" do
+ project_2 = create(:project)
+
label_1 = create(:label, title: 'label_1', project: project, priority: 1)
label_2 = create(:label, title: 'label_2', project: project, priority: 2)
label_3 = create(:label, title: 'label_3', project: project, priority: 3)
+ label_1_2 = create(:label, title: 'label_1', project: project_2, priority: 1)
issue_1 = create(:issue, title: 'issue_1', project: project)
issue_2 = create(:issue, title: 'issue_2', project: project)
issue_3 = create(:issue, title: 'issue_3', project: project)
issue_4 = create(:issue, title: 'issue_4', project: project)
- merge_request_1 = create(:merge_request, source_project: project)
+ merge_request_1 = create(:merge_request, source_project: project_2)
- merge_request_1.labels << label_1
+ merge_request_1.labels << label_1_2
# Covers the case where Todo has more than one label
issue_3.labels << label_1
@@ -57,15 +61,14 @@ describe TodosFinder do
todo_2 = create(:todo, user: user, project: project, target: issue_2)
todo_3 = create(:todo, user: user, project: project, target: issue_3, created_at: 2.hours.ago)
todo_4 = create(:todo, user: user, project: project, target: issue_1)
- todo_5 = create(:todo, user: user, project: project, target: merge_request_1, created_at: 1.hour.ago)
+ todo_5 = create(:todo, user: user, project: project_2, target: merge_request_1, created_at: 1.hour.ago)
+
+ project_2.add_developer(user)
todos = finder.new(user, { sort: 'priority' }).execute
- expect(todos.first).to eq(todo_3)
- expect(todos.second).to eq(todo_5)
- expect(todos.third).to eq(todo_4)
- expect(todos.fourth).to eq(todo_2)
- expect(todos.fifth).to eq(todo_1)
+ puts todos.to_sql
+ expect(todos).to eq([todo_3, todo_5, todo_4, todo_2, todo_1])
end
end
end
diff --git a/spec/fixtures/api/schemas/cluster_status.json b/spec/fixtures/api/schemas/cluster_status.json
index 489d563be2b..d27c12e43f2 100644
--- a/spec/fixtures/api/schemas/cluster_status.json
+++ b/spec/fixtures/api/schemas/cluster_status.json
@@ -30,7 +30,8 @@
]
}
},
- "status_reason": { "type": ["string", "null"] }
+ "status_reason": { "type": ["string", "null"] },
+ "external_ip": { "type": ["string", "null"] }
},
"required" : [ "name", "status" ]
}
diff --git a/spec/fixtures/api/schemas/entities/merge_request_widget.json b/spec/fixtures/api/schemas/entities/merge_request_widget.json
index 05461787f06..cfbeec58a45 100644
--- a/spec/fixtures/api/schemas/entities/merge_request_widget.json
+++ b/spec/fixtures/api/schemas/entities/merge_request_widget.json
@@ -75,7 +75,9 @@
"properties": {
"can_remove_source_branch": { "type": "boolean" },
"can_revert_on_current_merge_request": { "type": ["boolean", "null"] },
- "can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] }
+ "can_cherry_pick_on_current_merge_request": { "type": ["boolean", "null"] },
+ "can_create_note": { "type": "boolean" },
+ "can_update": { "type": "boolean" }
},
"additionalProperties": false
},
@@ -103,6 +105,7 @@
"merge_ongoing": { "type": "boolean" },
"ff_only_enabled": { "type": ["boolean", false] },
"should_be_rebased": { "type": "boolean" },
+ "create_note_path": { "type": ["string", "null"] },
"rebase_commit_sha": { "type": ["string", "null"] },
"rebase_in_progress": { "type": "boolean" },
"can_push_to_source_branch": { "type": "boolean" },
diff --git a/spec/fixtures/emails/update_commands_only_reply.eml b/spec/fixtures/emails/update_commands_only_reply.eml
new file mode 100644
index 00000000000..bb0d2b0e03a
--- /dev/null
+++ b/spec/fixtures/emails/update_commands_only_reply.eml
@@ -0,0 +1,38 @@
+Return-Path: <jake@adventuretime.ooo>
+Received: from iceking.adventuretime.ooo ([unix socket]) by iceking (Cyrus v2.2.13-Debian-2.2.13-19+squeeze3) with LMTPA; Thu, 13 Jun 2013 17:03:50 -0400
+Received: from mail-ie0-x234.google.com (mail-ie0-x234.google.com [IPv6:2607:f8b0:4001:c03::234]) by iceking.adventuretime.ooo (8.14.3/8.14.3/Debian-9.4) with ESMTP id r5DL3nFJ016967 (version=TLSv1/SSLv3 cipher=RC4-SHA bits=128 verify=NOT) for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 17:03:50 -0400
+Received: by mail-ie0-f180.google.com with SMTP id f4so21977375iea.25 for <reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo>; Thu, 13 Jun 2013 14:03:48 -0700
+Received: by 10.0.0.1 with HTTP; Thu, 13 Jun 2013 14:03:48 -0700
+Date: Thu, 13 Jun 2013 17:03:48 -0400
+From: Jake the Dog <jake@adventuretime.ooo>
+To: reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo
+Message-ID: <CADkmRc+rNGAGGbV2iE5p918UVy4UyJqVcXRO2=otppgzduJSg@mail.gmail.com>
+In-Reply-To: <issue_1@localhost>
+References: <issue_1@localhost> <reply-59d8df8370b7e95c5a49fbf86aeb2c93@localhost>
+Subject: re: [Discourse Meta] eviltrout posted in 'Adventure Time Sux'
+Mime-Version: 1.0
+Content-Type: text/plain;
+ charset=ISO-8859-1
+Content-Transfer-Encoding: 7bit
+X-Sieve: CMU Sieve 2.2
+X-Received: by 10.0.0.1 with SMTP id n7mr11234144ipb.85.1371157428600; Thu,
+ 13 Jun 2013 14:03:48 -0700 (PDT)
+X-Scanned-By: MIMEDefang 2.69 on IPv6:2001:470:1d:165::1
+
+/close
+
+On Sun, Jun 9, 2013 at 1:39 PM, eviltrout via Discourse Meta
+<reply+59d8df8370b7e95c5a49fbf86aeb2c93@appmail.adventuretime.ooo> wrote:
+>
+>
+>
+> eviltrout posted in 'Adventure Time Sux' on Discourse Meta:
+>
+> ---
+> hey guys everyone knows adventure time sucks!
+>
+> ---
+> Please visit this link to respond: http://localhost:3000/t/adventure-time-sux/1234/3
+>
+> To unsubscribe from these emails, visit your [user preferences](http://localhost:3000/user_preferences).
+>
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index a030796c54e..1fa194fe1b8 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -86,7 +86,7 @@ describe BlobHelper do
it 'verifies blob is text' do
expect(helper).not_to receive(:blob_text_viewable?)
- button = edit_blob_link(project, 'refs/heads/master', 'README.md')
+ button = edit_blob_button(project, 'refs/heads/master', 'README.md')
expect(button).to start_with('<button')
end
@@ -96,17 +96,17 @@ describe BlobHelper do
expect(project.repository).not_to receive(:blob_at)
- edit_blob_link(project, 'refs/heads/master', 'README.md', blob: blob)
+ edit_blob_button(project, 'refs/heads/master', 'README.md', blob: blob)
end
it 'returns a link with the proper route' do
- link = edit_blob_link(project, 'master', 'README.md')
+ link = edit_blob_button(project, 'master', 'README.md')
expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md")
end
it 'returns a link with the passed link_opts on the expected route' do
- link = edit_blob_link(project, 'master', 'README.md', link_opts: { mr_id: 10 })
+ link = edit_blob_button(project, 'master', 'README.md', link_opts: { mr_id: 10 })
expect(Capybara.string(link).find_link('Edit')[:href]).to eq("/#{project.full_path}/edit/master/README.md?mr_id=10")
end
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 7fa665aecdc..2fecd1a3d27 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -173,23 +173,23 @@ describe IssuablesHelper do
@project = issue.project
expected_data = {
- 'endpoint' => "/#{@project.full_path}/issues/#{issue.iid}",
- 'updateEndpoint' => "/#{@project.full_path}/issues/#{issue.iid}.json",
- 'canUpdate' => true,
- 'canDestroy' => true,
- 'issuableRef' => "##{issue.iid}",
- 'markdownPreviewPath' => "/#{@project.full_path}/preview_markdown",
- 'markdownDocsPath' => '/help/user/markdown',
- 'issuableTemplates' => [],
- 'projectPath' => @project.path,
- 'projectNamespace' => @project.namespace.path,
- 'initialTitleHtml' => issue.title,
- 'initialTitleText' => issue.title,
- 'initialDescriptionHtml' => '<p dir="auto">issue text</p>',
- 'initialDescriptionText' => 'issue text',
- 'initialTaskStatus' => '0 of 0 tasks completed'
+ endpoint: "/#{@project.full_path}/issues/#{issue.iid}",
+ updateEndpoint: "/#{@project.full_path}/issues/#{issue.iid}.json",
+ canUpdate: true,
+ canDestroy: true,
+ issuableRef: "##{issue.iid}",
+ markdownPreviewPath: "/#{@project.full_path}/preview_markdown",
+ markdownDocsPath: '/help/user/markdown',
+ issuableTemplates: [],
+ projectPath: @project.path,
+ projectNamespace: @project.namespace.path,
+ initialTitleHtml: issue.title,
+ initialTitleText: issue.title,
+ initialDescriptionHtml: '<p dir="auto">issue text</p>',
+ initialDescriptionText: 'issue text',
+ initialTaskStatus: '0 of 0 tasks completed'
}
- expect(JSON.parse(helper.issuable_initial_data(issue))).to eq(expected_data)
+ expect(helper.issuable_initial_data(issue)).to eq(expected_data)
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 45ffbeb27a4..4590904c93d 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -12,10 +12,10 @@ describe MembersHelper do
let(:group_member_invite) { build(:group_member, group: group).tap { |m| m.generate_invite_token! } }
let(:group_member_request) { group.request_access(requester) }
- it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.name_with_namespace} project?" }
- it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.name_with_namespace} project?" }
- it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.name_with_namespace} project?" }
- it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.name_with_namespace} project?" }
+ it { expect(remove_member_message(project_member)).to eq "Are you sure you want to remove #{project_member.user.name} from the #{project.full_name} project?" }
+ it { expect(remove_member_message(project_member_invite)).to eq "Are you sure you want to revoke the invitation for #{project_member_invite.invite_email} to join the #{project.full_name} project?" }
+ it { expect(remove_member_message(project_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{project.full_name} project?" }
+ it { expect(remove_member_message(project_member_request, user: requester)).to eq "Are you sure you want to withdraw your access request for the #{project.full_name} project?" }
it { expect(remove_member_message(group_member)).to eq "Are you sure you want to remove #{group_member.user.name} from the #{group.name} group?" }
it { expect(remove_member_message(group_member_invite)).to eq "Are you sure you want to revoke the invitation for #{group_member_invite.invite_email} to join the #{group.name} group?" }
it { expect(remove_member_message(group_member_request)).to eq "Are you sure you want to deny #{requester.name}'s request to join the #{group.name} group?" }
@@ -42,7 +42,7 @@ describe MembersHelper do
let(:group) { build_stubbed(:group) }
let(:user) { build_stubbed(:user) }
- it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.name_with_namespace}\" project?" }
+ it { expect(leave_confirmation_message(project)).to eq "Are you sure you want to leave the \"#{project.full_name}\" project?" }
it { expect(leave_confirmation_message(group)).to eq "Are you sure you want to leave the \"#{group.name}\" group?" }
end
end
diff --git a/spec/helpers/todos_helper_spec.rb b/spec/helpers/todos_helper_spec.rb
index f55163c26e9..63806ef91f3 100644
--- a/spec/helpers/todos_helper_spec.rb
+++ b/spec/helpers/todos_helper_spec.rb
@@ -26,8 +26,8 @@ describe TodosHelper do
expected_results = [
{ 'id' => '', 'text' => 'Any Project' },
- { 'id' => projects.second.id, 'text' => projects.second.name_with_namespace },
- { 'id' => projects.first.id, 'text' => projects.first.name_with_namespace }
+ { 'id' => projects.second.id, 'text' => projects.second.full_name },
+ { 'id' => projects.first.id, 'text' => projects.first.full_name }
]
expect(JSON.parse(helper.todo_projects_options)).to match_array(expected_results)
diff --git a/spec/helpers/u2f_helper_spec.rb b/spec/helpers/u2f_helper_spec.rb
deleted file mode 100644
index 0d65b4fe0b8..00000000000
--- a/spec/helpers/u2f_helper_spec.rb
+++ /dev/null
@@ -1,49 +0,0 @@
-require 'spec_helper'
-
-describe U2fHelper do
- describe 'when not on mobile' do
- it 'does not inject u2f on chrome 40' do
- device = double(mobile?: false)
- browser = double(chrome?: true, opera?: false, version: 40, device: device)
- allow(helper).to receive(:browser).and_return(browser)
- expect(helper.inject_u2f_api?).to eq false
- end
-
- it 'injects u2f on chrome 41' do
- device = double(mobile?: false)
- browser = double(chrome?: true, opera?: false, version: 41, device: device)
- allow(helper).to receive(:browser).and_return(browser)
- expect(helper.inject_u2f_api?).to eq true
- end
-
- it 'does not inject u2f on opera 39' do
- device = double(mobile?: false)
- browser = double(chrome?: false, opera?: true, version: 39, device: device)
- allow(helper).to receive(:browser).and_return(browser)
- expect(helper.inject_u2f_api?).to eq false
- end
-
- it 'injects u2f on opera 40' do
- device = double(mobile?: false)
- browser = double(chrome?: false, opera?: true, version: 40, device: device)
- allow(helper).to receive(:browser).and_return(browser)
- expect(helper.inject_u2f_api?).to eq true
- end
- end
-
- describe 'when on mobile' do
- it 'does not inject u2f on chrome 41' do
- device = double(mobile?: true)
- browser = double(chrome?: true, opera?: false, version: 41, device: device)
- allow(helper).to receive(:browser).and_return(browser)
- expect(helper.inject_u2f_api?).to eq false
- end
-
- it 'does not inject u2f on opera 40' do
- device = double(mobile?: true)
- browser = double(chrome?: false, opera?: true, version: 40, device: device)
- allow(helper).to receive(:browser).and_return(browser)
- expect(helper.inject_u2f_api?).to eq false
- end
- end
-end
diff --git a/spec/javascripts/autosave_spec.js b/spec/javascripts/autosave_spec.js
index 9f9acc392c2..b568d7fa8b0 100644
--- a/spec/javascripts/autosave_spec.js
+++ b/spec/javascripts/autosave_spec.js
@@ -3,28 +3,24 @@ import AccessorUtilities from '~/lib/utils/accessor';
describe('Autosave', () => {
let autosave;
+ const field = $('<textarea></textarea>');
+ const key = 'key';
describe('class constructor', () => {
- const key = 'key';
- const field = jasmine.createSpyObj('field', ['data', 'on']);
-
beforeEach(() => {
spyOn(AccessorUtilities, 'isLocalStorageAccessSafe').and.returnValue(true);
spyOn(Autosave.prototype, 'restore');
-
- autosave = new Autosave(field, key);
});
it('should set .isLocalStorageAvailable', () => {
+ autosave = new Autosave(field, key);
+
expect(AccessorUtilities.isLocalStorageAccessSafe).toHaveBeenCalled();
expect(autosave.isLocalStorageAvailable).toBe(true);
});
});
describe('restore', () => {
- const key = 'key';
- const field = jasmine.createSpyObj('field', ['trigger']);
-
beforeEach(() => {
autosave = {
field,
@@ -49,24 +45,53 @@ describe('Autosave', () => {
describe('if .isLocalStorageAvailable is `true`', () => {
beforeEach(() => {
autosave.isLocalStorageAvailable = true;
-
- Autosave.prototype.restore.call(autosave);
});
it('should call .getItem', () => {
+ Autosave.prototype.restore.call(autosave);
+
expect(window.localStorage.getItem).toHaveBeenCalledWith(key);
});
+
+ it('triggers jquery event', () => {
+ spyOn(autosave.field, 'trigger').and.callThrough();
+
+ Autosave.prototype.restore.call(autosave);
+
+ expect(
+ field.trigger,
+ ).toHaveBeenCalled();
+ });
+
+ it('triggers native event', (done) => {
+ autosave.field.get(0).addEventListener('change', () => {
+ done();
+ });
+
+ Autosave.prototype.restore.call(autosave);
+ });
+ });
+
+ describe('if field gets deleted from DOM', () => {
+ beforeEach(() => {
+ autosave.field = $('.not-a-real-element');
+ });
+
+ it('does not trigger event', () => {
+ spyOn(field, 'trigger').and.callThrough();
+
+ expect(
+ field.trigger,
+ ).not.toHaveBeenCalled();
+ });
});
});
describe('save', () => {
- const field = jasmine.createSpyObj('field', ['val']);
-
beforeEach(() => {
autosave = jasmine.createSpyObj('autosave', ['reset']);
autosave.field = field;
-
- field.val.and.returnValue('value');
+ field.val('value');
spyOn(window.localStorage, 'setItem');
});
@@ -97,8 +122,6 @@ describe('Autosave', () => {
});
describe('reset', () => {
- const key = 'key';
-
beforeEach(() => {
autosave = {
key,
diff --git a/spec/javascripts/boards/board_card_spec.js b/spec/javascripts/boards/board_card_spec.js
index 80a598e63bd..13d607a06d2 100644
--- a/spec/javascripts/boards/board_card_spec.js
+++ b/spec/javascripts/boards/board_card_spec.js
@@ -9,8 +9,8 @@ import axios from '~/lib/utils/axios_utils';
import '~/boards/models/assignee';
import eventHub from '~/boards/eventhub';
+import '~/vue_shared/models/label';
import '~/boards/models/list';
-import '~/boards/models/label';
import '~/boards/stores/boards_store';
import boardCard from '~/boards/components/board_card.vue';
import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/board_new_issue_spec.js b/spec/javascripts/boards/board_new_issue_spec.js
index e204985f039..d5fbfdeaa91 100644
--- a/spec/javascripts/boards/board_new_issue_spec.js
+++ b/spec/javascripts/boards/board_new_issue_spec.js
@@ -4,7 +4,7 @@
import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
-import boardNewIssue from '~/boards/components/board_new_issue';
+import boardNewIssue from '~/boards/components/board_new_issue.vue';
import '~/boards/models/list';
import { listObj, boardsMockInterceptor, mockBoardService } from './mock_data';
diff --git a/spec/javascripts/boards/boards_store_spec.js b/spec/javascripts/boards/boards_store_spec.js
index 8411f4dd8a6..0cf9e4c9ba1 100644
--- a/spec/javascripts/boards/boards_store_spec.js
+++ b/spec/javascripts/boards/boards_store_spec.js
@@ -7,8 +7,8 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Cookies from 'js-cookie';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/issue_card_spec.js b/spec/javascripts/boards/issue_card_spec.js
index 278155c585e..37088a6421c 100644
--- a/spec/javascripts/boards/issue_card_spec.js
+++ b/spec/javascripts/boards/issue_card_spec.js
@@ -4,8 +4,8 @@
import Vue from 'vue';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/stores/boards_store';
diff --git a/spec/javascripts/boards/issue_spec.js b/spec/javascripts/boards/issue_spec.js
index dbbe14fe3e0..4a11131b55c 100644
--- a/spec/javascripts/boards/issue_spec.js
+++ b/spec/javascripts/boards/issue_spec.js
@@ -3,8 +3,8 @@
/* global ListIssue */
import Vue from 'vue';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/list_spec.js b/spec/javascripts/boards/list_spec.js
index 34964b20b05..d9a1d692949 100644
--- a/spec/javascripts/boards/list_spec.js
+++ b/spec/javascripts/boards/list_spec.js
@@ -6,8 +6,8 @@
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import _ from 'underscore';
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/services/board_service';
diff --git a/spec/javascripts/boards/modal_store_spec.js b/spec/javascripts/boards/modal_store_spec.js
index 7eecb58a4c3..e9d77f035e3 100644
--- a/spec/javascripts/boards/modal_store_spec.js
+++ b/spec/javascripts/boards/modal_store_spec.js
@@ -1,7 +1,7 @@
/* global ListIssue */
+import '~/vue_shared/models/label';
import '~/boards/models/issue';
-import '~/boards/models/label';
import '~/boards/models/list';
import '~/boards/models/assignee';
import '~/boards/stores/modal_store';
diff --git a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
index cac785fd3c6..270f925e699 100644
--- a/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
+++ b/spec/javascripts/ci_variable_list/ci_variable_list_spec.js
@@ -1,5 +1,5 @@
import VariableList from '~/ci_variable_list/ci_variable_list';
-import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
const HIDE_CLASS = 'hide';
diff --git a/spec/javascripts/clusters/clusters_bundle_spec.js b/spec/javascripts/clusters/clusters_bundle_spec.js
index a9e244e523d..a5cd247b689 100644
--- a/spec/javascripts/clusters/clusters_bundle_spec.js
+++ b/spec/javascripts/clusters/clusters_bundle_spec.js
@@ -7,7 +7,7 @@ import {
REQUEST_SUCCESS,
REQUEST_FAILURE,
} from '~/clusters/constants';
-import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('Clusters', () => {
let cluster;
diff --git a/spec/javascripts/clusters/components/application_row_spec.js b/spec/javascripts/clusters/components/application_row_spec.js
index e671c18e1a5..2c4707bb856 100644
--- a/spec/javascripts/clusters/components/application_row_spec.js
+++ b/spec/javascripts/clusters/components/application_row_spec.js
@@ -12,7 +12,7 @@ import {
REQUEST_FAILURE,
} from '~/clusters/constants';
import applicationRow from '~/clusters/components/application_row.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { DEFAULT_APPLICATION_STATE } from '../services/mock_data';
describe('Application Row', () => {
diff --git a/spec/javascripts/clusters/components/applications_spec.js b/spec/javascripts/clusters/components/applications_spec.js
index 1a8affad4e3..d546543d273 100644
--- a/spec/javascripts/clusters/components/applications_spec.js
+++ b/spec/javascripts/clusters/components/applications_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import applications from '~/clusters/components/applications.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Applications', () => {
let vm;
@@ -38,10 +38,75 @@ describe('Applications', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-prometheus')).toBeDefined();
});
- /* * /
it('renders a row for GitLab Runner', () => {
expect(vm.$el.querySelector('.js-cluster-application-row-runner')).toBeDefined();
});
- /* */
+ });
+
+ describe('Ingress application', () => {
+ describe('when installed', () => {
+ describe('with ip address', () => {
+ it('renders ip address with a clipboard button', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ ingress: {
+ title: 'Ingress',
+ status: 'installed',
+ externalIp: '0.0.0.0',
+ },
+ helm: { title: 'Helm Tiller' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ },
+ });
+
+ expect(
+ vm.$el.querySelector('.js-ip-address').value,
+ ).toEqual('0.0.0.0');
+
+ expect(
+ vm.$el.querySelector('.js-clipboard-btn').getAttribute('data-clipboard-text'),
+ ).toEqual('0.0.0.0');
+ });
+ });
+
+ describe('without ip address', () => {
+ it('renders an input text with a question mark and an alert text', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ ingress: {
+ title: 'Ingress',
+ status: 'installed',
+ },
+ helm: { title: 'Helm Tiller' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ },
+ });
+
+ expect(
+ vm.$el.querySelector('.js-ip-address').value,
+ ).toEqual('?');
+
+ expect(vm.$el.querySelector('.js-no-ip-message')).not.toBe(null);
+ });
+ });
+ });
+
+ describe('before installing', () => {
+ it('does not render the IP address', () => {
+ vm = mountComponent(Applications, {
+ applications: {
+ helm: { title: 'Helm Tiller' },
+ ingress: { title: 'Ingress' },
+ runner: { title: 'GitLab Runner' },
+ prometheus: { title: 'Prometheus' },
+ },
+ });
+
+ expect(vm.$el.textContent).not.toContain('Ingress IP Address');
+ expect(vm.$el.querySelector('.js-ip-address')).toBe(null);
+ });
+ });
});
});
diff --git a/spec/javascripts/clusters/services/mock_data.js b/spec/javascripts/clusters/services/mock_data.js
index 253b3c45243..6ae7a792329 100644
--- a/spec/javascripts/clusters/services/mock_data.js
+++ b/spec/javascripts/clusters/services/mock_data.js
@@ -18,6 +18,7 @@ const CLUSTERS_MOCK_DATA = {
name: 'ingress',
status: APPLICATION_ERROR,
status_reason: 'Cannot connect',
+ external_ip: null,
}, {
name: 'runner',
status: APPLICATION_INSTALLING,
diff --git a/spec/javascripts/clusters/stores/clusters_store_spec.js b/spec/javascripts/clusters/stores/clusters_store_spec.js
index 726a4ed30de..8028faf2f02 100644
--- a/spec/javascripts/clusters/stores/clusters_store_spec.js
+++ b/spec/javascripts/clusters/stores/clusters_store_spec.js
@@ -75,6 +75,7 @@ describe('Clusters Store', () => {
statusReason: mockResponseData.applications[1].status_reason,
requestStatus: null,
requestReason: null,
+ externalIp: null,
},
runner: {
title: 'GitLab Runner',
diff --git a/spec/javascripts/commit/commit_pipeline_status_component_spec.js b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
index 90f290e845e..421fe62a1e7 100644
--- a/spec/javascripts/commit/commit_pipeline_status_component_spec.js
+++ b/spec/javascripts/commit/commit_pipeline_status_component_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import commitPipelineStatus from '~/projects/tree/components/commit_pipeline_status_component.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Commit pipeline status component', () => {
let vm;
diff --git a/spec/javascripts/cycle_analytics/banner_spec.js b/spec/javascripts/cycle_analytics/banner_spec.js
index 64a76a6ee5f..2815bdba0c2 100644
--- a/spec/javascripts/cycle_analytics/banner_spec.js
+++ b/spec/javascripts/cycle_analytics/banner_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import banner from '~/cycle_analytics/components/banner.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Cycle analytics banner', () => {
let vm;
diff --git a/spec/javascripts/cycle_analytics/total_time_component_spec.js b/spec/javascripts/cycle_analytics/total_time_component_spec.js
index ad0fc38a856..691e03cb8a6 100644
--- a/spec/javascripts/cycle_analytics/total_time_component_spec.js
+++ b/spec/javascripts/cycle_analytics/total_time_component_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import component from '~/cycle_analytics/components/total_time_component.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Total time component', () => {
let vm;
diff --git a/spec/javascripts/environments/emtpy_state_spec.js b/spec/javascripts/environments/emtpy_state_spec.js
index 82de35933f5..10a19af4175 100644
--- a/spec/javascripts/environments/emtpy_state_spec.js
+++ b/spec/javascripts/environments/emtpy_state_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import emptyState from '~/environments/components/empty_state.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('environments empty state', () => {
let vm;
diff --git a/spec/javascripts/environments/environment_table_spec.js b/spec/javascripts/environments/environment_table_spec.js
index 9bd42863759..0e5e50a59a5 100644
--- a/spec/javascripts/environments/environment_table_spec.js
+++ b/spec/javascripts/environments/environment_table_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import environmentTableComp from '~/environments/components/environments_table.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Environment table', () => {
let Component;
diff --git a/spec/javascripts/environments/environments_app_spec.js b/spec/javascripts/environments/environments_app_spec.js
index a41a4e5a3f7..5bb37304372 100644
--- a/spec/javascripts/environments/environments_app_spec.js
+++ b/spec/javascripts/environments/environments_app_spec.js
@@ -1,9 +1,9 @@
import _ from 'underscore';
import Vue from 'vue';
import environmentsComponent from '~/environments/components/environments_app.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
+import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
import { environment, folder } from './mock_data';
-import { headersInterceptor } from '../helpers/vue_resource_helper';
-import mountComponent from '../helpers/vue_mount_component_helper';
describe('Environment', () => {
const mockData = {
diff --git a/spec/javascripts/environments/folder/environments_folder_view_spec.js b/spec/javascripts/environments/folder/environments_folder_view_spec.js
index a085074d312..906a1116974 100644
--- a/spec/javascripts/environments/folder/environments_folder_view_spec.js
+++ b/spec/javascripts/environments/folder/environments_folder_view_spec.js
@@ -1,9 +1,9 @@
import _ from 'underscore';
import Vue from 'vue';
import environmentsFolderViewComponent from '~/environments/folder/environments_folder_view.vue';
+import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { environmentsList } from '../mock_data';
-import { headersInterceptor } from '../../helpers/vue_resource_helper';
-import mountComponent from '../../helpers/vue_mount_component_helper';
describe('Environments Folder View', () => {
let Component;
diff --git a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
index 34ffc7b1016..1b1f28f3ddb 100644
--- a/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
+++ b/spec/javascripts/feature_highlight/feature_highlight_helper_spec.js
@@ -8,7 +8,7 @@ import {
mouseenter,
inserted,
} from '~/feature_highlight/feature_highlight_helper';
-import getSetTimeoutPromise from '../helpers/set_timeout_promise_helper';
+import getSetTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
describe('feature highlight helper', () => {
describe('getSelector', () => {
diff --git a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
index 4a516c517ef..59bd2650081 100644
--- a/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
+++ b/spec/javascripts/filtered_search/components/recent_searches_dropdown_content_spec.js
@@ -1,7 +1,6 @@
import Vue from 'vue';
import eventHub from '~/filtered_search/event_hub';
-import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content';
-
+import RecentSearchesDropdownContent from '~/filtered_search/components/recent_searches_dropdown_content.vue';
import FilteredSearchTokenKeys from '~/filtered_search/filtered_search_token_keys';
const createComponent = (propsData) => {
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index f1da5f81c0f..756a654765b 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -128,6 +128,24 @@ describe('Filtered Search Visual Tokens', () => {
});
});
+ describe('getEndpointWithQueryParams', () => {
+ it('returns `endpoint` string as is when second param `endpointQueryParams` is undefined, null or empty string', () => {
+ const endpoint = 'foo/bar/labels.json';
+ expect(subject.getEndpointWithQueryParams(endpoint)).toBe(endpoint);
+ expect(subject.getEndpointWithQueryParams(endpoint, null)).toBe(endpoint);
+ expect(subject.getEndpointWithQueryParams(endpoint, '')).toBe(endpoint);
+ });
+
+ it('returns `endpoint` string with values of `endpointQueryParams`', () => {
+ const endpoint = 'foo/bar/labels.json';
+ const singleQueryParams = '{"foo":"true"}';
+ const multipleQueryParams = '{"foo":"true","bar":"true"}';
+
+ expect(subject.getEndpointWithQueryParams(endpoint, singleQueryParams)).toBe(`${endpoint}?foo=true`);
+ expect(subject.getEndpointWithQueryParams(endpoint, multipleQueryParams)).toBe(`${endpoint}?foo=true&bar=true`);
+ });
+ });
+
describe('unselectTokens', () => {
it('does nothing when there are no tokens', () => {
const beforeHTML = tokensContainer.innerHTML;
diff --git a/spec/javascripts/fixtures/merge_requests.rb b/spec/javascripts/fixtures/merge_requests.rb
index 3fd16d76f51..ee60489eb7c 100644
--- a/spec/javascripts/fixtures/merge_requests.rb
+++ b/spec/javascripts/fixtures/merge_requests.rb
@@ -70,8 +70,50 @@ describe Projects::MergeRequestsController, '(JavaScript fixtures)', type: :cont
render_merge_request(example.description, merge_request)
end
+ it 'merge_requests/discussions.json' do |example|
+ create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
+ render_discussions_json(merge_request, example.description)
+ end
+
+ it 'merge_requests/diff_discussion.json' do |example|
+ create(:diff_note_on_merge_request, project: project, author: admin, position: position, noteable: merge_request)
+ render_discussions_json(merge_request, example.description)
+ end
+
+ context 'with image diff' do
+ let(:merge_request2) { create(:merge_request_with_diffs, :with_image_diffs, source_project: project, title: "Added images") }
+ let(:image_path) { "files/images/ee_repo_logo.png" }
+ let(:image_position) do
+ Gitlab::Diff::Position.new(
+ old_path: image_path,
+ new_path: image_path,
+ width: 100,
+ height: 100,
+ x: 1,
+ y: 1,
+ position_type: "image",
+ diff_refs: merge_request2.diff_refs
+ )
+ end
+
+ it 'merge_requests/image_diff_discussion.json' do |example|
+ create(:diff_note_on_merge_request, project: project, noteable: merge_request2, position: image_position)
+ render_discussions_json(merge_request2, example.description)
+ end
+ end
+
private
+ def render_discussions_json(merge_request, fixture_file_name)
+ get :discussions,
+ namespace_id: project.namespace.to_param,
+ project_id: project,
+ id: merge_request.to_param,
+ format: :json
+
+ store_frontend_fixture(response, fixture_file_name)
+ end
+
def render_merge_request(fixture_file_name, merge_request)
get :show,
namespace_id: project.namespace.to_param,
diff --git a/spec/javascripts/groups/components/app_spec.js b/spec/javascripts/groups/components/app_spec.js
index 3adc29262f3..46c7b9f54f2 100644
--- a/spec/javascripts/groups/components/app_spec.js
+++ b/spec/javascripts/groups/components/app_spec.js
@@ -129,7 +129,7 @@ describe('AppComponent', () => {
vm.fetchGroups({});
setTimeout(() => {
- expect(vm.isLoading).toBeFalsy();
+ expect(vm.isLoading).toBe(false);
expect($.scrollTo).toHaveBeenCalledWith(0);
expect(window.Flash).toHaveBeenCalledWith('An error occurred. Please try again.');
done();
@@ -144,10 +144,10 @@ describe('AppComponent', () => {
spyOn(vm, 'updateGroups').and.callThrough();
vm.fetchAllGroups();
- expect(vm.isLoading).toBeTruthy();
+ expect(vm.isLoading).toBe(true);
expect(vm.fetchGroups).toHaveBeenCalled();
setTimeout(() => {
- expect(vm.isLoading).toBeFalsy();
+ expect(vm.isLoading).toBe(false);
expect(vm.updateGroups).toHaveBeenCalled();
done();
}, 0);
@@ -181,7 +181,7 @@ describe('AppComponent', () => {
spyOn($, 'scrollTo');
vm.fetchPage(2, null, null, true);
- expect(vm.isLoading).toBeTruthy();
+ expect(vm.isLoading).toBe(true);
expect(vm.fetchGroups).toHaveBeenCalledWith({
page: 2,
filterGroupsBy: null,
@@ -190,7 +190,7 @@ describe('AppComponent', () => {
archived: true,
});
setTimeout(() => {
- expect(vm.isLoading).toBeFalsy();
+ expect(vm.isLoading).toBe(false);
expect($.scrollTo).toHaveBeenCalledWith(0);
expect(utils.mergeUrlParams).toHaveBeenCalledWith({ page: 2 }, jasmine.any(String));
expect(window.history.replaceState).toHaveBeenCalledWith({
@@ -216,7 +216,7 @@ describe('AppComponent', () => {
spyOn(vm.store, 'setGroupChildren');
vm.toggleChildren(groupItem);
- expect(groupItem.isChildrenLoading).toBeTruthy();
+ expect(groupItem.isChildrenLoading).toBe(true);
expect(vm.fetchGroups).toHaveBeenCalledWith({
parentId: groupItem.id,
});
@@ -232,7 +232,7 @@ describe('AppComponent', () => {
vm.toggleChildren(groupItem);
expect(vm.fetchGroups).not.toHaveBeenCalled();
- expect(groupItem.isOpen).toBeTruthy();
+ expect(groupItem.isOpen).toBe(true);
});
it('should collapse group if it is already expanded', () => {
@@ -241,16 +241,16 @@ describe('AppComponent', () => {
vm.toggleChildren(groupItem);
expect(vm.fetchGroups).not.toHaveBeenCalled();
- expect(groupItem.isOpen).toBeFalsy();
+ expect(groupItem.isOpen).toBe(false);
});
it('should set `isChildrenLoading` back to `false` if load request fails', (done) => {
spyOn(vm, 'fetchGroups').and.returnValue(returnServicePromise({}, true));
vm.toggleChildren(groupItem);
- expect(groupItem.isChildrenLoading).toBeTruthy();
+ expect(groupItem.isChildrenLoading).toBe(true);
setTimeout(() => {
- expect(groupItem.isChildrenLoading).toBeFalsy();
+ expect(groupItem.isChildrenLoading).toBe(false);
done();
}, 0);
});
@@ -268,10 +268,10 @@ describe('AppComponent', () => {
it('updates props which show modal confirmation dialog', () => {
const group = Object.assign({}, mockParentGroupItem);
- expect(vm.updateModal).toBeFalsy();
+ expect(vm.showModal).toBe(false);
expect(vm.groupLeaveConfirmationMessage).toBe('');
vm.showLeaveGroupModal(group, mockParentGroupItem);
- expect(vm.updateModal).toBeTruthy();
+ expect(vm.showModal).toBe(true);
expect(vm.groupLeaveConfirmationMessage).toBe(`Are you sure you want to leave the "${group.fullName}" group?`);
});
});
@@ -280,9 +280,9 @@ describe('AppComponent', () => {
it('hides modal confirmation which is shown before leaving the group', () => {
const group = Object.assign({}, mockParentGroupItem);
vm.showLeaveGroupModal(group, mockParentGroupItem);
- expect(vm.updateModal).toBeTruthy();
+ expect(vm.showModal).toBe(true);
vm.hideLeaveGroupModal();
- expect(vm.updateModal).toBeFalsy();
+ expect(vm.showModal).toBe(false);
});
});
@@ -307,8 +307,8 @@ describe('AppComponent', () => {
spyOn($, 'scrollTo');
vm.leaveGroup();
- expect(vm.updateModal).toBeFalsy();
- expect(vm.targetGroup.isBeingRemoved).toBeTruthy();
+ expect(vm.showModal).toBe(false);
+ expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(vm.targetGroup.leavePath);
setTimeout(() => {
expect($.scrollTo).toHaveBeenCalledWith(0);
@@ -325,12 +325,12 @@ describe('AppComponent', () => {
spyOn(window, 'Flash');
vm.leaveGroup();
- expect(vm.targetGroup.isBeingRemoved).toBeTruthy();
+ expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
setTimeout(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
expect(window.Flash).toHaveBeenCalledWith(message);
- expect(vm.targetGroup.isBeingRemoved).toBeFalsy();
+ expect(vm.targetGroup.isBeingRemoved).toBe(false);
done();
}, 0);
});
@@ -342,12 +342,12 @@ describe('AppComponent', () => {
spyOn(window, 'Flash');
vm.leaveGroup(childGroupItem, groupItem);
- expect(vm.targetGroup.isBeingRemoved).toBeTruthy();
+ expect(vm.targetGroup.isBeingRemoved).toBe(true);
expect(vm.service.leaveGroup).toHaveBeenCalledWith(childGroupItem.leavePath);
setTimeout(() => {
expect(vm.store.removeGroup).not.toHaveBeenCalled();
expect(window.Flash).toHaveBeenCalledWith(message);
- expect(vm.targetGroup.isBeingRemoved).toBeFalsy();
+ expect(vm.targetGroup.isBeingRemoved).toBe(false);
done();
}, 0);
});
@@ -379,10 +379,10 @@ describe('AppComponent', () => {
it('should set `isSearchEmpty` prop based on groups count', () => {
vm.updateGroups(mockGroups);
- expect(vm.isSearchEmpty).toBeFalsy();
+ expect(vm.isSearchEmpty).toBe(false);
vm.updateGroups([]);
- expect(vm.isSearchEmpty).toBeTruthy();
+ expect(vm.isSearchEmpty).toBe(true);
});
});
});
@@ -473,13 +473,16 @@ describe('AppComponent', () => {
});
});
- it('renders modal confirmation dialog', () => {
+ it('renders modal confirmation dialog', (done) => {
vm.groupLeaveConfirmationMessage = 'Are you sure you want to leave the "foo" group?';
- vm.updateModal = true;
- const modalDialogEl = vm.$el.querySelector('.modal');
- expect(modalDialogEl).not.toBe(null);
- expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
- expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
+ vm.showModal = true;
+ Vue.nextTick(() => {
+ const modalDialogEl = vm.$el.querySelector('.modal');
+ expect(modalDialogEl).not.toBe(null);
+ expect(modalDialogEl.querySelector('.modal-title').innerText.trim()).toBe('Are you sure?');
+ expect(modalDialogEl.querySelector('.btn.btn-warning').innerText.trim()).toBe('Leave');
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/groups/components/group_item_spec.js b/spec/javascripts/groups/components/group_item_spec.js
index 618d0022e4f..e3c942597a3 100644
--- a/spec/javascripts/groups/components/group_item_spec.js
+++ b/spec/javascripts/groups/components/group_item_spec.js
@@ -3,10 +3,9 @@ import * as urlUtils from '~/lib/utils/url_utility';
import groupItemComponent from '~/groups/components/group_item.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import eventHub from '~/groups/event_hub';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
const Component = Vue.extend(groupItemComponent);
diff --git a/spec/javascripts/groups/components/groups_spec.js b/spec/javascripts/groups/components/groups_spec.js
index 90e818c1545..793c4909d89 100644
--- a/spec/javascripts/groups/components/groups_spec.js
+++ b/spec/javascripts/groups/components/groups_spec.js
@@ -4,10 +4,9 @@ import groupsComponent from '~/groups/components/groups.vue';
import groupFolderComponent from '~/groups/components/group_folder.vue';
import groupItemComponent from '~/groups/components/group_item.vue';
import eventHub from '~/groups/event_hub';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockGroups, mockPageInfo } from '../mock_data';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
const createComponent = (searchEmpty = false) => {
const Component = Vue.extend(groupsComponent);
diff --git a/spec/javascripts/groups/components/item_actions_spec.js b/spec/javascripts/groups/components/item_actions_spec.js
index acccbe639c4..15fd37ebcd2 100644
--- a/spec/javascripts/groups/components/item_actions_spec.js
+++ b/spec/javascripts/groups/components/item_actions_spec.js
@@ -2,10 +2,9 @@ import Vue from 'vue';
import itemActionsComponent from '~/groups/components/item_actions.vue';
import eventHub from '~/groups/event_hub';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockParentGroupItem, mockChildren } from '../mock_data';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
const createComponent = (group = mockParentGroupItem, parentGroup = mockChildren[0]) => {
const Component = Vue.extend(itemActionsComponent);
diff --git a/spec/javascripts/groups/components/item_caret_spec.js b/spec/javascripts/groups/components/item_caret_spec.js
index 8faad455825..36f838a104f 100644
--- a/spec/javascripts/groups/components/item_caret_spec.js
+++ b/spec/javascripts/groups/components/item_caret_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import itemCaretComponent from '~/groups/components/item_caret.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (isGroupOpen = false) => {
const Component = Vue.extend(itemCaretComponent);
diff --git a/spec/javascripts/groups/components/item_stats_spec.js b/spec/javascripts/groups/components/item_stats_spec.js
index 55a7a713ca6..ee7ee18259e 100644
--- a/spec/javascripts/groups/components/item_stats_spec.js
+++ b/spec/javascripts/groups/components/item_stats_spec.js
@@ -1,6 +1,7 @@
import Vue from 'vue';
import itemStatsComponent from '~/groups/components/item_stats.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import {
mockParentGroupItem,
ITEM_TYPE,
@@ -9,8 +10,6 @@ import {
PROJECT_VISIBILITY_TYPE,
} from '../mock_data';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
const createComponent = (item = mockParentGroupItem) => {
const Component = Vue.extend(itemStatsComponent);
diff --git a/spec/javascripts/groups/components/item_stats_value_spec.js b/spec/javascripts/groups/components/item_stats_value_spec.js
index e990870aaa6..5e35ae4d36c 100644
--- a/spec/javascripts/groups/components/item_stats_value_spec.js
+++ b/spec/javascripts/groups/components/item_stats_value_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import itemStatsValueComponent from '~/groups/components/item_stats_value.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = ({ title, cssClass, iconName, tooltipPlacement, value }) => {
const Component = Vue.extend(itemStatsValueComponent);
diff --git a/spec/javascripts/groups/components/item_type_icon_spec.js b/spec/javascripts/groups/components/item_type_icon_spec.js
index 495cc97b475..24380689b29 100644
--- a/spec/javascripts/groups/components/item_type_icon_spec.js
+++ b/spec/javascripts/groups/components/item_type_icon_spec.js
@@ -1,10 +1,9 @@
import Vue from 'vue';
import itemTypeIconComponent from '~/groups/components/item_type_icon.vue';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { ITEM_TYPE } from '../mock_data';
-import mountComponent from '../../helpers/vue_mount_component_helper';
-
const createComponent = (itemType = ITEM_TYPE.GROUP, isGroupOpen = false) => {
const Component = Vue.extend(itemTypeIconComponent);
diff --git a/spec/javascripts/issue_show/components/app_spec.js b/spec/javascripts/issue_show/components/app_spec.js
index 1c9f48028f2..584db6c6632 100644
--- a/spec/javascripts/issue_show/components/app_spec.js
+++ b/spec/javascripts/issue_show/components/app_spec.js
@@ -6,8 +6,8 @@ import '~/render_gfm';
import * as urlUtils from '~/lib/utils/url_utility';
import issuableApp from '~/issue_show/components/app.vue';
import eventHub from '~/issue_show/event_hub';
+import setTimeoutPromise from 'spec/helpers/set_timeout_promise_helper';
import issueShowData from '../mock_data';
-import setTimeoutPromise from '../../helpers/set_timeout_promise_helper';
function formatText(text) {
return text.trim().replace(/\s\s+/g, ' ');
diff --git a/spec/javascripts/issue_show/components/description_spec.js b/spec/javascripts/issue_show/components/description_spec.js
index 0da25bdca9c..ff7f99eec14 100644
--- a/spec/javascripts/issue_show/components/description_spec.js
+++ b/spec/javascripts/issue_show/components/description_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import descriptionComponent from '~/issue_show/components/description.vue';
import * as taskList from '~/task_list';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Description component', () => {
let vm;
diff --git a/spec/javascripts/jobs/header_spec.js b/spec/javascripts/jobs/header_spec.js
index a9df0418d5d..0961605ce5c 100644
--- a/spec/javascripts/jobs/header_spec.js
+++ b/spec/javascripts/jobs/header_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import headerComponent from '~/jobs/components/header.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Job details header', () => {
let HeaderComponent;
diff --git a/spec/javascripts/labels_select_spec.js b/spec/javascripts/labels_select_spec.js
new file mode 100644
index 00000000000..b8f7b1dc855
--- /dev/null
+++ b/spec/javascripts/labels_select_spec.js
@@ -0,0 +1,43 @@
+import LabelsSelect from '~/labels_select';
+
+const mockUrl = '/foo/bar/url';
+
+const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+describe('LabelsSelect', () => {
+ describe('getLabelTemplate', () => {
+ const label = mockLabels[0];
+ let $labelEl;
+
+ beforeEach(() => {
+ $labelEl = $(LabelsSelect.getLabelTemplate({
+ labels: mockLabels,
+ issueUpdateURL: mockUrl,
+ }));
+ });
+
+ it('generated label item template has correct label URL', () => {
+ expect($labelEl.attr('href')).toBe('/foo/bar?label_name[]=Foo%20Label');
+ });
+
+ it('generated label item template has correct label title', () => {
+ expect($labelEl.find('span.label').text()).toBe(label.title);
+ });
+
+ it('generated label item template has label description as title attribute', () => {
+ expect($labelEl.find('span.label').attr('title')).toBe(label.description);
+ });
+
+ it('generated label item template has correct label styles', () => {
+ expect($labelEl.find('span.label').attr('style')).toBe(`background-color: ${label.color}; color: ${label.text_color};`);
+ });
+ });
+});
diff --git a/spec/javascripts/lib/utils/common_utils_spec.js b/spec/javascripts/lib/utils/common_utils_spec.js
index 49799c31995..27f06573432 100644
--- a/spec/javascripts/lib/utils/common_utils_spec.js
+++ b/spec/javascripts/lib/utils/common_utils_spec.js
@@ -166,6 +166,21 @@ describe('common_utils', () => {
});
});
+ describe('objectToQueryString', () => {
+ it('returns empty string when `param` is undefined, null or empty string', () => {
+ expect(commonUtils.objectToQueryString()).toBe('');
+ expect(commonUtils.objectToQueryString('')).toBe('');
+ });
+
+ it('returns query string with values of `params`', () => {
+ const singleQueryParams = { foo: true };
+ const multipleQueryParams = { foo: true, bar: true };
+
+ expect(commonUtils.objectToQueryString(singleQueryParams)).toBe('foo=true');
+ expect(commonUtils.objectToQueryString(multipleQueryParams)).toBe('foo=true&bar=true');
+ });
+ });
+
describe('buildUrlWithCurrentLocation', () => {
it('should build an url with current location and given parameters', () => {
expect(commonUtils.buildUrlWithCurrentLocation()).toEqual(window.location.pathname);
diff --git a/spec/javascripts/notes/components/comment_form_spec.js b/spec/javascripts/notes/components/comment_form_spec.js
index 104d03377b6..6a7131528a3 100644
--- a/spec/javascripts/notes/components/comment_form_spec.js
+++ b/spec/javascripts/notes/components/comment_form_spec.js
@@ -1,17 +1,20 @@
import Vue from 'vue';
import Autosize from 'autosize';
import store from '~/notes/stores';
-import issueCommentForm from '~/notes/components/comment_form.vue';
+import CommentForm from '~/notes/components/comment_form.vue';
import { loggedOutnoteableData, notesDataMock, userDataMock, noteableDataMock } from '../mock_data';
import { keyboardDownEvent } from '../../issue_show/helpers';
describe('issue_comment_form component', () => {
let vm;
- const Component = Vue.extend(issueCommentForm);
+ const Component = Vue.extend(CommentForm);
let mountComponent;
beforeEach(() => {
- mountComponent = () => new Component({
+ mountComponent = (noteableType = 'issue') => new Component({
+ propsData: {
+ noteableType,
+ },
store,
}).$mount();
});
@@ -136,6 +139,11 @@ describe('issue_comment_form component', () => {
expect(vm.editCurrentUserLastNote).toHaveBeenCalled();
});
+
+ it('inits autosave', () => {
+ expect(vm.autosave).toBeDefined();
+ expect(vm.autosave.key).toEqual(`autosave/Note/Issue/${noteableDataMock.id}`);
+ });
});
describe('event enter', () => {
@@ -182,6 +190,15 @@ describe('issue_comment_form component', () => {
done();
});
});
+
+ it('updates button text with noteable type', (done) => {
+ vm.noteableType = 'merge_request';
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn-comment-and-close').textContent.trim()).toEqual('Close merge request');
+ done();
+ });
+ });
});
describe('issue is confidential', () => {
diff --git a/spec/javascripts/notes/components/diff_file_header_spec.js b/spec/javascripts/notes/components/diff_file_header_spec.js
new file mode 100644
index 00000000000..aed30a087a6
--- /dev/null
+++ b/spec/javascripts/notes/components/diff_file_header_spec.js
@@ -0,0 +1,93 @@
+import Vue from 'vue';
+import DiffFileHeader from '~/notes/components/diff_file_header.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const discussionFixture = 'merge_requests/diff_discussion.json';
+
+describe('diff_file_header', () => {
+ let vm;
+ const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
+ const diffFile = convertObjectPropsToCamelCase(diffDiscussionMock.diff_file);
+ const props = {
+ diffFile,
+ };
+ const Component = Vue.extend(DiffFileHeader);
+ const selectors = {
+ get copyButton() {
+ return vm.$el.querySelector('button[data-original-title="Copy file path to clipboard"]');
+ },
+ get fileName() {
+ return vm.$el.querySelector('.file-title-name');
+ },
+ get titleWrapper() {
+ return vm.$refs.titleWrapper;
+ },
+ };
+
+ describe('submodule', () => {
+ beforeEach(() => {
+ props.diffFile.submodule = true;
+ props.diffFile.submoduleLink = '<a href="/bha">Submodule</a>';
+
+ vm = mountComponent(Component, props);
+ });
+
+ it('shows submoduleLink', () => {
+ expect(selectors.fileName.innerHTML).toBe(props.diffFile.submoduleLink);
+ });
+
+ it('has button to copy blob path', () => {
+ expect(selectors.copyButton).toExist();
+ expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.submoduleLink);
+ });
+ });
+
+ describe('changed file', () => {
+ beforeEach(() => {
+ props.diffFile.submodule = false;
+ props.diffFile.discussionPath = 'some/discussion/id';
+
+ vm = mountComponent(Component, props);
+ });
+
+ it('shows file type icon', () => {
+ expect(vm.$el.innerHTML).toContain('fa-file-text-o');
+ });
+
+ it('links to discussion path', () => {
+ expect(selectors.titleWrapper).toExist();
+ expect(selectors.titleWrapper.tagName).toBe('A');
+ expect(selectors.titleWrapper.getAttribute('href')).toBe(props.diffFile.discussionPath);
+ });
+
+ it('shows plain title if no link given', () => {
+ props.diffFile.discussionPath = undefined;
+ vm = mountComponent(Component, props);
+
+ expect(selectors.titleWrapper.tagName).not.toBe('A');
+ expect(selectors.titleWrapper.href).toBeFalsy();
+ });
+
+ it('has button to copy file path', () => {
+ expect(selectors.copyButton).toExist();
+ expect(selectors.copyButton.getAttribute('data-clipboard-text')).toBe(props.diffFile.filePath);
+ });
+
+ it('shows file mode change', (done) => {
+ vm.diffFile = {
+ ...props.diffFile,
+ modeChanged: true,
+ aMode: '100755',
+ bMode: '100644',
+ };
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$refs.fileMode.textContent.trim(),
+ ).toBe('100755 → 100644');
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/diff_with_note_spec.js b/spec/javascripts/notes/components/diff_with_note_spec.js
new file mode 100644
index 00000000000..7f1f4bf0bcd
--- /dev/null
+++ b/spec/javascripts/notes/components/diff_with_note_spec.js
@@ -0,0 +1,64 @@
+import Vue from 'vue';
+import DiffWithNote from '~/notes/components/diff_with_note.vue';
+import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+const discussionFixture = 'merge_requests/diff_discussion.json';
+const imageDiscussionFixture = 'merge_requests/image_diff_discussion.json';
+
+describe('diff_with_note', () => {
+ let vm;
+ const diffDiscussionMock = getJSONFixture(discussionFixture)[0];
+ const diffDiscussion = convertObjectPropsToCamelCase(diffDiscussionMock);
+ const Component = Vue.extend(DiffWithNote);
+ const props = {
+ discussion: diffDiscussion,
+ };
+ const selectors = {
+ get container() {
+ return vm.$refs.fileHolder;
+ },
+ get diffTable() {
+ return this.container.querySelector('.diff-content table');
+ },
+ get diffRows() {
+ return this.container.querySelectorAll('.diff-content .line_holder');
+ },
+ get noteRow() {
+ return this.container.querySelector('.diff-content .notes_holder');
+ },
+ };
+
+ describe('text diff', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, props);
+ });
+
+ it('shows text diff', () => {
+ expect(selectors.container).toHaveClass('text-file');
+ expect(selectors.diffTable).toExist();
+ });
+
+ it('shows diff lines', () => {
+ expect(selectors.diffRows.length).toBe(12);
+ });
+
+ it('shows notes row', () => {
+ expect(selectors.noteRow).toExist();
+ });
+ });
+
+ describe('image diff', () => {
+ beforeEach(() => {
+ const imageDiffDiscussionMock = getJSONFixture(imageDiscussionFixture)[0];
+ props.discussion = convertObjectPropsToCamelCase(imageDiffDiscussionMock);
+ });
+
+ it('shows image diff', () => {
+ vm = mountComponent(Component, props);
+
+ expect(selectors.container).toHaveClass('js-image-file');
+ expect(selectors.diffTable).not.toExist();
+ });
+ });
+});
diff --git a/spec/javascripts/notes/components/note_app_spec.js b/spec/javascripts/notes/components/note_app_spec.js
index 12d180137a0..e1c612f5100 100644
--- a/spec/javascripts/notes/components/note_app_spec.js
+++ b/spec/javascripts/notes/components/note_app_spec.js
@@ -24,6 +24,7 @@ describe('note_app', () => {
beforeEach(() => {
jasmine.addMatchers(vueMatchers);
+ $('body').attr('data-page', 'projects:merge_requests:show');
const IssueNotesApp = Vue.extend(notesApp);
@@ -119,8 +120,8 @@ describe('note_app', () => {
vm = mountComponent();
});
- it('should render loading icon', () => {
- expect(vm).toIncludeElement('.js-loading');
+ it('renders skeleton notes', () => {
+ expect(vm).toIncludeElement('.animation-container');
});
it('should render form', () => {
diff --git a/spec/javascripts/notes/components/note_body_spec.js b/spec/javascripts/notes/components/note_body_spec.js
index b42e7943b98..0ff804f0e55 100644
--- a/spec/javascripts/notes/components/note_body_spec.js
+++ b/spec/javascripts/notes/components/note_body_spec.js
@@ -30,17 +30,26 @@ describe('issue_note_body component', () => {
expect(vm.$el.querySelector('.note-text').innerHTML).toEqual(note.note_html);
});
- it('should be render form if user is editing', (done) => {
- vm.isEditing = true;
+ it('should render awards list', () => {
+ expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).not.toBeNull();
+ });
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('textarea.js-task-list-field')).toBeDefined();
- done();
+ describe('isEditing', () => {
+ beforeEach((done) => {
+ vm.isEditing = true;
+ Vue.nextTick(done);
});
- });
- it('should render awards list', () => {
- expect(vm.$el.querySelector('.js-awards-block button [data-name="baseball"]')).toBeDefined();
- expect(vm.$el.querySelector('.js-awards-block button [data-name="bath_tone3"]')).toBeDefined();
+ it('renders edit form', () => {
+ expect(vm.$el.querySelector('textarea.js-task-list-field')).not.toBeNull();
+ });
+
+ it('adds autosave', () => {
+ const autosaveKey = `autosave/Note/${note.noteable_type}/${note.id}`;
+
+ expect(vm.autosave).toExist();
+ expect(vm.autosave.key).toEqual(autosaveKey);
+ });
});
});
diff --git a/spec/javascripts/notes/components/note_header_spec.js b/spec/javascripts/notes/components/note_header_spec.js
index 16a76b11321..5636f8d1a9f 100644
--- a/spec/javascripts/notes/components/note_header_spec.js
+++ b/spec/javascripts/notes/components/note_header_spec.js
@@ -32,6 +32,7 @@ describe('note_header component', () => {
createdAt: '2017-08-02T10:51:58.559Z',
includeToggle: false,
noteId: 1394,
+ expanded: true,
},
}).$mount();
});
@@ -68,6 +69,7 @@ describe('note_header component', () => {
createdAt: '2017-08-02T10:51:58.559Z',
includeToggle: true,
noteId: 1395,
+ expanded: true,
},
}).$mount();
});
@@ -76,17 +78,35 @@ describe('note_header component', () => {
expect(vm.$el.querySelector('.js-vue-toggle-button')).toBeDefined();
});
- it('should toggle the disucssion icon', (done) => {
- expect(
- vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-up'),
- ).toEqual(true);
+ it('emits toggle event on click', (done) => {
+ spyOn(vm, '$emit');
vm.$el.querySelector('.js-vue-toggle-button').click();
Vue.nextTick(() => {
+ expect(vm.$emit).toHaveBeenCalledWith('toggleHandler');
+ done();
+ });
+ });
+
+ it('renders up arrow when open', (done) => {
+ vm.expanded = true;
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.js-vue-toggle-button i').classList,
+ ).toContain('fa-chevron-up');
+ done();
+ });
+ });
+
+ it('renders down arrow when closed', (done) => {
+ vm.expanded = false;
+
+ Vue.nextTick(() => {
expect(
- vm.$el.querySelector('.js-vue-toggle-button i').classList.contains('fa-chevron-down'),
- ).toEqual(true);
+ vm.$el.querySelector('.js-vue-toggle-button i').classList,
+ ).toContain('fa-chevron-down');
done();
});
});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index ccf4bd070c2..bf60cb12f52 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -7,8 +7,9 @@ export const notesDataMock = {
notesPath: '/gitlab-org/gitlab-ce/noteable/issue/98/notes',
quickActionsDocsPath: '/help/user/project/quick_actions',
registerPath: '/users/sign_in?redirect_to_referer=yes#register-pane',
- closeIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
- reopenIssuePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
+ totalNotes: 1,
+ closePath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=close',
+ reopenPath: '/twitter/flight/issues/9.json?issue%5Bstate_event%5D=reopen',
};
export const userDataMock = {
diff --git a/spec/javascripts/notes/stores/getters_spec.js b/spec/javascripts/notes/stores/getters_spec.js
index 919ffbfdef0..8b2a8d2cd7a 100644
--- a/spec/javascripts/notes/stores/getters_spec.js
+++ b/spec/javascripts/notes/stores/getters_spec.js
@@ -56,9 +56,9 @@ describe('Getters Notes Store', () => {
});
});
- describe('issueState', () => {
+ describe('openState', () => {
it('should return the issue state', () => {
- expect(getters.issueState(state)).toEqual(noteableDataMock.state);
+ expect(getters.openState(state)).toEqual(noteableDataMock.state);
});
});
});
diff --git a/spec/javascripts/notes/stores/mutation_spec.js b/spec/javascripts/notes/stores/mutation_spec.js
index 22d99998a7d..e4baefc5bfc 100644
--- a/spec/javascripts/notes/stores/mutation_spec.js
+++ b/spec/javascripts/notes/stores/mutation_spec.js
@@ -1,7 +1,7 @@
import mutations from '~/notes/stores/mutations';
import { note, discussionMock, notesDataMock, userDataMock, noteableDataMock, individualNote } from '../mock_data';
-describe('Mutation Notes Store', () => {
+describe('Notes Store mutations', () => {
describe('ADD_NEW_NOTE', () => {
let state;
let noteData;
@@ -103,7 +103,8 @@ describe('Mutation Notes Store', () => {
};
mutations.SET_INITIAL_NOTES(state, [note]);
- expect(state.notes).toEqual([note]);
+ expect(state.notes[0].id).toEqual(note.id);
+ expect(state.notes.length).toEqual(1);
});
});
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 274d7591c71..d4a148e6ab1 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -34,6 +34,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
describe('Notes', function() {
const FLASH_TYPE_ALERT = 'alert';
+ const NOTES_POST_PATH = /(.*)\/notes\?html=true$/;
var commentsTemplate = 'merge_requests/merge_request_with_comment.html.raw';
preloadFixtures(commentsTemplate);
@@ -154,7 +155,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
$form.find('textarea.js-note-text').val(sampleComment);
mock = new MockAdapter(axios);
- mock.onPost(/(.*)\/notes$/).reply(200, noteEntity);
+ mock.onPost(NOTES_POST_PATH).reply(200, noteEntity);
});
afterEach(() => {
@@ -506,11 +507,11 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
let mock;
function mockNotesPost() {
- mock.onPost(/(.*)\/notes$/).reply(200, note);
+ mock.onPost(NOTES_POST_PATH).reply(200, note);
}
function mockNotesPostError() {
- mock.onPost(/(.*)\/notes$/).networkError();
+ mock.onPost(NOTES_POST_PATH).networkError();
}
beforeEach(() => {
@@ -631,7 +632,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onPost(/(.*)\/notes$/).reply(200, note);
+ mock.onPost(NOTES_POST_PATH).reply(200, note);
this.notes = new Notes('', []);
window.gon.current_username = 'root';
@@ -684,7 +685,7 @@ import timeoutPromise from './helpers/set_timeout_promise_helper';
beforeEach(() => {
mock = new MockAdapter(axios);
- mock.onPost(/(.*)\/notes$/).reply(200, note);
+ mock.onPost(NOTES_POST_PATH).reply(200, note);
this.notes = new Notes('', []);
window.gon.current_username = 'root';
diff --git a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
index 440a6585d57..a6fe9fb65e9 100644
--- a/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
+++ b/spec/javascripts/pages/admin/jobs/index/components/stop_jobs_modal_spec.js
@@ -4,7 +4,7 @@ import axios from '~/lib/utils/axios_utils';
import stopJobsModal from '~/pages/admin/jobs/index/components/stop_jobs_modal.vue';
import * as urlUtility from '~/lib/utils/url_utility';
-import mountComponent from '../../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('stop_jobs_modal.vue', () => {
const props = {
diff --git a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
index 3cd33a3e900..6074e06fcec 100644
--- a/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
+++ b/spec/javascripts/pages/milestones/shared/components/delete_milestone_modal_spec.js
@@ -5,7 +5,7 @@ import deleteMilestoneModal from '~/pages/milestones/shared/components/delete_mi
import eventHub from '~/pages/milestones/shared/event_hub';
import * as urlUtility from '~/lib/utils/url_utility';
-import mountComponent from '../../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('delete_milestone_modal.vue', () => {
const Component = Vue.extend(deleteMilestoneModal);
diff --git a/spec/javascripts/pipelines/blank_state_spec.js b/spec/javascripts/pipelines/blank_state_spec.js
new file mode 100644
index 00000000000..b7a9b60d85c
--- /dev/null
+++ b/spec/javascripts/pipelines/blank_state_spec.js
@@ -0,0 +1,29 @@
+import Vue from 'vue';
+import component from '~/pipelines/components/blank_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Pipelines Blank State', () => {
+ let vm;
+ let Component;
+
+ beforeEach(() => {
+ Component = Vue.extend(component);
+
+ vm = mountComponent(Component,
+ {
+ svgPath: 'foo',
+ message: 'Blank State',
+ },
+ );
+ });
+
+ it('should render svg', () => {
+ expect(vm.$el.querySelector('.svg-content img').getAttribute('src')).toEqual('foo');
+ });
+
+ it('should render message', () => {
+ expect(
+ vm.$el.querySelector('h4').textContent.trim(),
+ ).toEqual('Blank State');
+ });
+});
diff --git a/spec/javascripts/pipelines/empty_state_spec.js b/spec/javascripts/pipelines/empty_state_spec.js
index 97f04844b3a..71f77e5f42e 100644
--- a/spec/javascripts/pipelines/empty_state_spec.js
+++ b/spec/javascripts/pipelines/empty_state_spec.js
@@ -1,5 +1,6 @@
import Vue from 'vue';
import emptyStateComp from '~/pipelines/components/empty_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Empty State', () => {
let component;
@@ -8,12 +9,15 @@ describe('Pipelines Empty State', () => {
beforeEach(() => {
EmptyStateComponent = Vue.extend(emptyStateComp);
- component = new EmptyStateComponent({
- propsData: {
- helpPagePath: 'foo',
- emptyStateSvgPath: 'foo',
- },
- }).$mount();
+ component = mountComponent(EmptyStateComponent, {
+ helpPagePath: 'foo',
+ emptyStateSvgPath: 'foo',
+ canSetCi: true,
+ });
+ });
+
+ afterEach(() => {
+ component.$destroy();
});
it('should render empty state SVG', () => {
@@ -24,16 +28,16 @@ describe('Pipelines Empty State', () => {
expect(component.$el.querySelector('h4').textContent).toContain('Build with confidence');
expect(
- component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '),
- ).toContain('Continous Integration can help catch bugs by running your tests automatically');
+ component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
+ ).toContain('Continous Integration can help catch bugs by running your tests automatically,');
expect(
- component.$el.querySelector('p').textContent.trim().replace(/[\r\n]+/g, ' '),
- ).toContain('Continuous Deployment can help you deliver code to your product environment');
+ component.$el.querySelector('p').innerHTML.trim().replace(/\n+\s+/m, ' ').replace(/\s\s+/g, ' '),
+ ).toContain('while Continuous Deployment can help you deliver code to your product environment');
});
it('should render a link with provided help path', () => {
- expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual('foo');
- expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
+ expect(component.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual('foo');
+ expect(component.$el.querySelector('.js-get-started-pipelines').textContent).toContain('Get started with Pipelines');
});
});
diff --git a/spec/javascripts/pipelines/error_state_spec.js b/spec/javascripts/pipelines/error_state_spec.js
deleted file mode 100644
index a402857a4d1..00000000000
--- a/spec/javascripts/pipelines/error_state_spec.js
+++ /dev/null
@@ -1,27 +0,0 @@
-import Vue from 'vue';
-import errorStateComp from '~/pipelines/components/error_state.vue';
-
-describe('Pipelines Error State', () => {
- let component;
- let ErrorStateComponent;
-
- beforeEach(() => {
- ErrorStateComponent = Vue.extend(errorStateComp);
-
- component = new ErrorStateComponent({
- propsData: {
- errorStateSvgPath: 'foo',
- },
- }).$mount();
- });
-
- it('should render error state SVG', () => {
- expect(component.$el.querySelector('.svg-content svg')).toBeDefined();
- });
-
- it('should render emtpy state information', () => {
- expect(
- component.$el.querySelector('h4').textContent,
- ).toContain('The API failed to fetch the pipelines');
- });
-});
diff --git a/spec/javascripts/pipelines/graph/job_component_spec.js b/spec/javascripts/pipelines/graph/job_component_spec.js
index c3dc7b53d0f..ce181a1e515 100644
--- a/spec/javascripts/pipelines/graph/job_component_spec.js
+++ b/spec/javascripts/pipelines/graph/job_component_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import jobComponent from '~/pipelines/components/graph/job_component.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('pipeline graph job component', () => {
let JobComponent;
diff --git a/spec/javascripts/pipelines/nav_controls_spec.js b/spec/javascripts/pipelines/nav_controls_spec.js
index 09a0c14d96c..77c5258f74c 100644
--- a/spec/javascripts/pipelines/nav_controls_spec.js
+++ b/spec/javascripts/pipelines/nav_controls_spec.js
@@ -1,116 +1,68 @@
import Vue from 'vue';
import navControlsComp from '~/pipelines/components/nav_controls.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
describe('Pipelines Nav Controls', () => {
let NavControlsComponent;
+ let component;
beforeEach(() => {
NavControlsComponent = Vue.extend(navControlsComp);
});
+ afterEach(() => {
+ component.$destroy();
+ });
+
it('should render link to create a new pipeline', () => {
const mockData = {
newPipelinePath: 'foo',
- hasCiEnabled: true,
- helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: true,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelector('.btn-create').textContent).toContain('Run Pipeline');
- expect(component.$el.querySelector('.btn-create').getAttribute('href')).toEqual(mockData.newPipelinePath);
+ expect(component.$el.querySelector('.js-run-pipeline').textContent).toContain('Run Pipeline');
+ expect(component.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(mockData.newPipelinePath);
});
- it('should not render link to create pipeline if no permission is provided', () => {
+ it('should not render link to create pipeline if no path is provided', () => {
const mockData = {
- newPipelinePath: 'foo',
- hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: false,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelector('.btn-create')).toEqual(null);
+ expect(component.$el.querySelector('.js-run-pipeline')).toEqual(null);
});
it('should render link for resetting runner caches', () => {
const mockData = {
newPipelinePath: 'foo',
- hasCiEnabled: true,
- helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: false,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelectorAll('.btn-default')[0].textContent).toContain('Clear runner caches');
- expect(component.$el.querySelectorAll('.btn-default')[0].getAttribute('href')).toEqual(mockData.resetCachePath);
+ expect(component.$el.querySelector('.js-clear-cache').textContent.trim()).toContain('Clear Runner Caches');
+ expect(component.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(mockData.resetCachePath);
});
it('should render link for CI lint', () => {
const mockData = {
newPipelinePath: 'foo',
- hasCiEnabled: true,
- helpPagePath: 'foo',
- ciLintPath: 'foo',
- resetCachePath: 'foo',
- canCreatePipeline: true,
- };
-
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
-
- expect(component.$el.querySelectorAll('.btn-default')[1].textContent).toContain('CI Lint');
- expect(component.$el.querySelectorAll('.btn-default')[1].getAttribute('href')).toEqual(mockData.ciLintPath);
- });
-
- it('should render link to help page when CI is not enabled', () => {
- const mockData = {
- newPipelinePath: 'foo',
- hasCiEnabled: false,
- helpPagePath: 'foo',
- ciLintPath: 'foo',
- resetCachePath: 'foo',
- canCreatePipeline: true,
- };
-
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
-
- expect(component.$el.querySelector('.btn-info').textContent).toContain('Get started with Pipelines');
- expect(component.$el.querySelector('.btn-info').getAttribute('href')).toEqual(mockData.helpPagePath);
- });
-
- it('should not render link to help page when CI is enabled', () => {
- const mockData = {
- newPipelinePath: 'foo',
- hasCiEnabled: true,
helpPagePath: 'foo',
ciLintPath: 'foo',
resetCachePath: 'foo',
- canCreatePipeline: true,
};
- const component = new NavControlsComponent({
- propsData: mockData,
- }).$mount();
+ component = mountComponent(NavControlsComponent, mockData);
- expect(component.$el.querySelector('.btn-info')).toEqual(null);
+ expect(component.$el.querySelector('.js-ci-lint').textContent.trim()).toContain('CI Lint');
+ expect(component.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(mockData.ciLintPath);
});
});
diff --git a/spec/javascripts/pipelines/pipelines_spec.js b/spec/javascripts/pipelines/pipelines_spec.js
index a99ebc4e51a..84fd0329f08 100644
--- a/spec/javascripts/pipelines/pipelines_spec.js
+++ b/spec/javascripts/pipelines/pipelines_spec.js
@@ -2,41 +2,385 @@ import _ from 'underscore';
import Vue from 'vue';
import pipelinesComp from '~/pipelines/components/pipelines.vue';
import Store from '~/pipelines/stores/pipelines_store';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Pipelines', () => {
const jsonFixtureName = 'pipelines/pipelines.json';
- preloadFixtures('static/pipelines.html.raw');
preloadFixtures(jsonFixtureName);
let PipelinesComponent;
let pipelines;
- let component;
+ let vm;
+ const paths = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ ciLintPath: '/ci/lint',
+ resetCachePath: '/twitter/flight/settings/ci_cd/reset_cache',
+ newPipelinePath: '/twitter/flight/pipelines/new',
+ };
+
+ const noPermissions = {
+ endpoint: 'twitter/flight/pipelines.json',
+ autoDevopsPath: '/help/topics/autodevops/index.md',
+ helpPagePath: '/help/ci/quick_start/README',
+ emptyStateSvgPath: '/assets/illustrations/pipelines_empty.svg',
+ errorStateSvgPath: '/assets/illustrations/pipelines_failed.svg',
+ noPipelinesSvgPath: '/assets/illustrations/pipelines_pending.svg',
+ };
beforeEach(() => {
- loadFixtures('static/pipelines.html.raw');
pipelines = getJSONFixture(jsonFixtureName);
PipelinesComponent = Vue.extend(pipelinesComp);
});
afterEach(() => {
- component.$destroy();
+ vm.$destroy();
+ });
+
+ const pipelinesInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify(pipelines), {
+ status: 200,
+ }));
+ };
+
+ const emptyStateInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({
+ pipelines: [],
+ count: {
+ all: 0,
+ pending: 0,
+ running: 0,
+ finished: 0,
+ },
+ }), {
+ status: 200,
+ }));
+ };
+
+ const errorInterceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({}), {
+ status: 500,
+ }));
+ };
+
+ describe('With permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(pipelinesInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('renders Run Pipeline button', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint button', () => {
+ expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
+ });
+
+ it('renders pipelines table', () => {
+ expect(
+ vm.$el.querySelectorAll('.gl-responsive-table-row').length,
+ ).toEqual(pipelines.pipelines.length + 1);
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('renders Run Pipeline button', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
+ });
+
+ it('renders CI Lint button', () => {
+ expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
+ });
+
+ it('renders Clear Runner Cache button', () => {
+ expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
+ });
+
+ it('renders tab empty state', () => {
+ expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+
+ it('renders empty state', () => {
+ expect(vm.$el.querySelector('.js-empty-state h4').textContent.trim()).toEqual('Build with confidence');
+ expect(vm.$el.querySelector('.js-get-started-pipelines').getAttribute('href')).toEqual(paths.helpPagePath);
+ });
+
+ it('does not render tabs nor buttons', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(errorInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: true,
+ ...paths,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, errorInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('renders buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline').getAttribute('href')).toEqual(paths.newPipelinePath);
+ expect(vm.$el.querySelector('.js-ci-lint').getAttribute('href')).toEqual(paths.ciLintPath);
+ expect(vm.$el.querySelector('.js-clear-cache').getAttribute('href')).toEqual(paths.resetCachePath);
+ });
+
+ it('renders error state', () => {
+ expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
+ });
+ });
+ });
+
+ describe('Without permission', () => {
+ describe('With pipelines in main tab', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(pipelinesInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: false,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, pipelinesInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+
+ it('renders pipelines table', () => {
+ expect(
+ vm.$el.querySelectorAll('.gl-responsive-table-row').length,
+ ).toEqual(pipelines.pipelines.length + 1);
+ });
+ });
+
+ describe('Without pipelines on main tab with CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: false,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('does not render buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+
+ it('renders tab empty state', () => {
+ expect(vm.$el.querySelector('.empty-state h4').textContent.trim()).toEqual('There are currently no pipelines.');
+ });
+ });
+
+ describe('Without pipelines nor CI', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(emptyStateInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: false,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, emptyStateInterceptor,
+ );
+ });
+
+ it('renders empty state without button to set CI', () => {
+ expect(vm.$el.querySelector('.js-empty-state').textContent.trim()).toEqual('This project is not currently set up to run pipelines.');
+ expect(vm.$el.querySelector('.js-get-started-pipelines')).toBeNull();
+ });
+
+ it('does not render tabs or buttons', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all')).toBeNull();
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+ });
+
+ describe('When API returns error', () => {
+ beforeEach((done) => {
+ Vue.http.interceptors.push(errorInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: false,
+ canCreatePipeline: true,
+ ...noPermissions,
+ });
+
+ setTimeout(() => {
+ done();
+ });
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(
+ Vue.http.interceptors, errorInterceptor,
+ );
+ });
+
+ it('renders tabs', () => {
+ expect(vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim()).toContain('All');
+ });
+
+ it('does not renders buttons', () => {
+ expect(vm.$el.querySelector('.js-run-pipeline')).toBeNull();
+ expect(vm.$el.querySelector('.js-ci-lint')).toBeNull();
+ expect(vm.$el.querySelector('.js-clear-cache')).toBeNull();
+ });
+
+ it('renders error state', () => {
+ expect(vm.$el.querySelector('.empty-state').textContent.trim()).toContain('There was an error fetching the pipelines.');
+ });
+ });
});
describe('successfull request', () => {
describe('with pipelines', () => {
- const pipelinesInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify(pipelines), {
- status: 200,
- }));
- };
-
beforeEach(() => {
Vue.http.interceptors.push(pipelinesInterceptor);
- component = mountComponent(PipelinesComponent, {
+ vm = mountComponent(PipelinesComponent, {
store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
});
});
@@ -48,9 +392,9 @@ describe('Pipelines', () => {
it('should render table', (done) => {
setTimeout(() => {
- expect(component.$el.querySelector('.table-holder')).toBeDefined();
+ expect(vm.$el.querySelector('.table-holder')).toBeDefined();
expect(
- component.$el.querySelectorAll('.gl-responsive-table-row').length,
+ vm.$el.querySelectorAll('.gl-responsive-table-row').length,
).toEqual(pipelines.pipelines.length + 1);
done();
});
@@ -59,22 +403,22 @@ describe('Pipelines', () => {
it('should render navigation tabs', (done) => {
setTimeout(() => {
expect(
- component.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-pending').textContent.trim(),
).toContain('Pending');
expect(
- component.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-all').textContent.trim(),
).toContain('All');
expect(
- component.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-running').textContent.trim(),
).toContain('Running');
expect(
- component.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-finished').textContent.trim(),
).toContain('Finished');
expect(
- component.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-branches').textContent.trim(),
).toContain('Branches');
expect(
- component.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
+ vm.$el.querySelector('.js-pipelines-tab-tags').textContent.trim(),
).toContain('Tags');
done();
});
@@ -82,10 +426,10 @@ describe('Pipelines', () => {
it('should make an API request when using tabs', (done) => {
setTimeout(() => {
- spyOn(component, 'updateContent');
- component.$el.querySelector('.js-pipelines-tab-finished').click();
+ spyOn(vm, 'updateContent');
+ vm.$el.querySelector('.js-pipelines-tab-finished').click();
- expect(component.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'finished', page: '1' });
done();
});
});
@@ -93,9 +437,9 @@ describe('Pipelines', () => {
describe('with pagination', () => {
it('should make an API request when using pagination', (done) => {
setTimeout(() => {
- spyOn(component, 'updateContent');
+ spyOn(vm, 'updateContent');
// Mock pagination
- component.store.state.pageInfo = {
+ vm.store.state.pageInfo = {
page: 1,
total: 10,
perPage: 2,
@@ -103,9 +447,9 @@ describe('Pipelines', () => {
totalPages: 5,
};
- Vue.nextTick(() => {
- component.$el.querySelector('.js-next-button a').click();
- expect(component.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
+ vm.$nextTick(() => {
+ vm.$el.querySelector('.js-next-button a').click();
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'all', page: '2' });
done();
});
@@ -113,112 +457,249 @@ describe('Pipelines', () => {
});
});
});
+ });
- describe('without pipelines', () => {
- const emptyInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 200,
- }));
- };
+ describe('methods', () => {
+ beforeEach(() => {
+ spyOn(history, 'pushState').and.stub();
+ });
- beforeEach(() => {
- Vue.http.interceptors.push(emptyInterceptor);
- });
+ describe('updateContent', () => {
+ it('should set given parameters', () => {
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+ vm.updateContent({ scope: 'finished', page: '4' });
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, emptyInterceptor,
- );
+ expect(vm.page).toEqual('4');
+ expect(vm.scope).toEqual('finished');
+ expect(vm.requestData.scope).toEqual('finished');
+ expect(vm.requestData.page).toEqual('4');
});
+ });
+
+ describe('onChangeTab', () => {
+ it('should set page to 1', () => {
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
+ spyOn(vm, 'updateContent');
- it('should render empty state', (done) => {
- component = new PipelinesComponent({
- propsData: {
- store: new Store(),
- },
- }).$mount();
+ vm.onChangeTab('running');
- setTimeout(() => {
- expect(component.$el.querySelector('.empty-state')).not.toBe(null);
- done();
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ });
+ });
+
+ describe('onChangePage', () => {
+ it('should update page and keep scope', () => {
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
});
+ spyOn(vm, 'updateContent');
+
+ vm.onChangePage(4);
+
+ expect(vm.updateContent).toHaveBeenCalledWith({ scope: vm.scope, page: '4' });
});
});
});
- describe('unsuccessfull request', () => {
- const errorInterceptor = (request, next) => {
- next(request.respondWith(JSON.stringify([]), {
- status: 500,
- }));
- };
-
+ describe('computed properties', () => {
beforeEach(() => {
- Vue.http.interceptors.push(errorInterceptor);
+ vm = mountComponent(PipelinesComponent, {
+ store: new Store(),
+ hasGitlabCi: true,
+ canCreatePipeline: true,
+ ...paths,
+ });
});
- afterEach(() => {
- Vue.http.interceptors = _.without(
- Vue.http.interceptors, errorInterceptor,
- );
+ describe('tabs', () => {
+ it('returns default tabs', () => {
+ expect(vm.tabs).toEqual([
+ { name: 'All', scope: 'all', count: undefined, isActive: true },
+ { name: 'Pending', scope: 'pending', count: undefined, isActive: false },
+ { name: 'Running', scope: 'running', count: undefined, isActive: false },
+ { name: 'Finished', scope: 'finished', count: undefined, isActive: false },
+ { name: 'Branches', scope: 'branches', isActive: false },
+ { name: 'Tags', scope: 'tags', isActive: false },
+ ]);
+ });
});
- it('should render error state', (done) => {
- component = new PipelinesComponent({
- propsData: {
- store: new Store(),
- },
- }).$mount();
+ describe('emptyTabMessage', () => {
+ it('returns message with scope', (done) => {
+ vm.scope = 'pending';
- setTimeout(() => {
- expect(component.$el.querySelector('.js-pipelines-error-state')).toBeDefined();
- done();
+ vm.$nextTick(() => {
+ expect(vm.emptyTabMessage).toEqual('There are currently no pending pipelines.');
+ done();
+ });
});
- });
- });
- describe('methods', () => {
- beforeEach(() => {
- spyOn(history, 'pushState').and.stub();
+ it('returns message without scope when scope is `all`', () => {
+ expect(vm.emptyTabMessage).toEqual('There are currently no pipelines.');
+ });
});
- describe('updateContent', () => {
- it('should set given parameters', () => {
- component = mountComponent(PipelinesComponent, {
- store: new Store(),
+ describe('stateToRender', () => {
+ it('returns loading state when the app is loading', () => {
+ expect(vm.stateToRender).toEqual('loading');
+ });
+
+ it('returns error state when app has error', (done) => {
+ vm.hasError = true;
+ vm.isLoading = false;
+
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('error');
+ done();
+ });
+ });
+
+ it('returns table list when app has pipelines', (done) => {
+ vm.isLoading = false;
+ vm.hasError = false;
+ vm.state.pipelines = pipelines.pipelines;
+
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('tableList');
+
+ done();
+ });
+ });
+
+ it('returns empty tab when app does not have pipelines but project has pipelines', (done) => {
+ vm.state.count.all = 10;
+ vm.isLoading = false;
+
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('emptyTab');
+
+ done();
});
- component.updateContent({ scope: 'finished', page: '4' });
+ });
+
+ it('returns empty tab when project has CI', (done) => {
+ vm.isLoading = false;
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('emptyTab');
- expect(component.page).toEqual('4');
- expect(component.scope).toEqual('finished');
- expect(component.requestData.scope).toEqual('finished');
- expect(component.requestData.page).toEqual('4');
+ done();
+ });
+ });
+
+ it('returns empty state when project does not have pipelines nor CI', (done) => {
+ vm.isLoading = false;
+ vm.hasGitlabCi = false;
+ vm.$nextTick(() => {
+ expect(vm.stateToRender).toEqual('emptyState');
+
+ done();
+ });
});
});
- describe('onChangeTab', () => {
- it('should set page to 1', () => {
- component = mountComponent(PipelinesComponent, {
- store: new Store(),
+ describe('shouldRenderTabs', () => {
+ it('returns true when state is loading & has already made the first request', (done) => {
+ vm.isLoading = true;
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
});
- spyOn(component, 'updateContent');
+ });
- component.onChangeTab('running');
+ it('returns true when state is tableList & has already made the first request', (done) => {
+ vm.isLoading = false;
+ vm.state.pipelines = pipelines.pipelines;
+ vm.hasMadeRequest = true;
- expect(component.updateContent).toHaveBeenCalledWith({ scope: 'running', page: '1' });
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
+ });
+ });
+
+ it('returns true when state is error & has already made the first request', (done) => {
+ vm.isLoading = false;
+ vm.hasError = true;
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
+ });
+ });
+
+ it('returns true when state is empty tab & has already made the first request', (done) => {
+ vm.isLoading = false;
+ vm.state.count.all = 10;
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(true);
+
+ done();
+ });
+ });
+
+ it('returns false when has not made first request', (done) => {
+ vm.hasMadeRequest = false;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(false);
+
+ done();
+ });
+ });
+
+ it('returns false when state is emtpy state', (done) => {
+ vm.isLoading = false;
+ vm.hasMadeRequest = true;
+ vm.hasGitlabCi = false;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderTabs).toEqual(false);
+
+ done();
+ });
});
});
- describe('onChangePage', () => {
- it('should update page and keep scope', () => {
- component = mountComponent(PipelinesComponent, {
- store: new Store(),
+ describe('shouldRenderButtons', () => {
+ it('returns true when it has paths & has made the first request', (done) => {
+ vm.hasMadeRequest = true;
+
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderButtons).toEqual(true);
+
+ done();
});
- spyOn(component, 'updateContent');
+ });
+
+ it('returns false when it has not made the first request', (done) => {
+ vm.hasMadeRequest = false;
- component.onChangePage(4);
+ vm.$nextTick(() => {
+ expect(vm.shouldRenderButtons).toEqual(false);
- expect(component.updateContent).toHaveBeenCalledWith({ scope: component.scope, page: '4' });
+ done();
+ });
});
});
});
diff --git a/spec/javascripts/profile/account/components/delete_account_modal_spec.js b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
index 588b61196a5..a0939ff5c20 100644
--- a/spec/javascripts/profile/account/components/delete_account_modal_spec.js
+++ b/spec/javascripts/profile/account/components/delete_account_modal_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import deleteAccountModal from '~/profile/account/components/delete_account_modal.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('DeleteAccountModal component', () => {
const actionUrl = `${gl.TEST_HOST}/delete/user`;
diff --git a/spec/javascripts/projects_dropdown/components/app_spec.js b/spec/javascripts/projects_dropdown/components/app_spec.js
index 42f0f6fc1af..2054fef790b 100644
--- a/spec/javascripts/projects_dropdown/components/app_spec.js
+++ b/spec/javascripts/projects_dropdown/components/app_spec.js
@@ -6,7 +6,7 @@ import eventHub from '~/projects_dropdown/event_hub';
import ProjectsStore from '~/projects_dropdown/store/projects_store';
import ProjectsService from '~/projects_dropdown/service/projects_service';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { currentSession, mockProject, mockRawProject } from '../mock_data';
const createComponent = () => {
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
index fcd0f6a3630..2bafb4e81ca 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
+++ b/spec/javascripts/projects_dropdown/components/projects_list_frequent_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import projectsListFrequentComponent from '~/projects_dropdown/components/projects_list_frequent.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockFrequents } from '../mock_data';
const createComponent = () => {
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
index edef150dd1e..c193258474e 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
+++ b/spec/javascripts/projects_dropdown/components/projects_list_item_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import projectsListItemComponent from '~/projects_dropdown/components/projects_list_item.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockProject } from '../mock_data';
const createComponent = () => {
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
index 67f8a8946c2..c4b86d77034 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
+++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import projectsListSearchComponent from '~/projects_dropdown/components/projects_list_search.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { mockProject } from '../mock_data';
const createComponent = () => {
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
index 24d8a00b254..601264258c2 100644
--- a/spec/javascripts/projects_dropdown/components/search_spec.js
+++ b/spec/javascripts/projects_dropdown/components/search_spec.js
@@ -3,7 +3,7 @@ import Vue from 'vue';
import searchComponent from '~/projects_dropdown/components/search.vue';
import eventHub from '~/projects_dropdown/event_hub';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = () => {
const Component = Vue.extend(searchComponent);
diff --git a/spec/javascripts/registry/components/app_spec.js b/spec/javascripts/registry/components/app_spec.js
index 6a8a85e3dfb..cf1d0625397 100644
--- a/spec/javascripts/registry/components/app_spec.js
+++ b/spec/javascripts/registry/components/app_spec.js
@@ -1,7 +1,7 @@
import _ from 'underscore';
import Vue from 'vue';
import registry from '~/registry/components/app.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import { reposServerResponse } from '../mock_data';
describe('Registry List', () => {
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
deleted file mode 100644
index debde1bb357..00000000000
--- a/spec/javascripts/repo/components/commit_sidebar/list_collapsed_spec.js
+++ /dev/null
@@ -1,33 +0,0 @@
-import Vue from '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';
-
-describe('Multi-file editor commit sidebar list collapsed', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(listCollapsed);
-
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.openFiles.push(file('file1'), file('file2'));
- vm.$store.state.openFiles[0].tempFile = true;
- vm.$store.state.openFiles.forEach((f) => {
- Object.assign(f, {
- changed: true,
- });
- });
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders added & modified files count', () => {
- expect(vm.$el.textContent.replace(/\s+/g, ' ').trim()).toBe('1 1');
- });
-});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
deleted file mode 100644
index 4b20fdf70d6..00000000000
--- a/spec/javascripts/repo/components/commit_sidebar/list_item_spec.js
+++ /dev/null
@@ -1,53 +0,0 @@
-import Vue from 'vue';
-import listItem from '~/ide/components/commit_sidebar/list_item.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
-import { file } from '../../helpers';
-
-describe('Multi-file editor commit sidebar list item', () => {
- let vm;
- let f;
-
- beforeEach(() => {
- const Component = Vue.extend(listItem);
-
- f = file('test-file');
-
- vm = mountComponent(Component, {
- file: f,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders file path', () => {
- expect(vm.$el.querySelector('.multi-file-commit-list-path').textContent.trim()).toBe(f.path);
- });
-
- describe('computed', () => {
- describe('iconName', () => {
- it('returns modified when not a tempFile', () => {
- expect(vm.iconName).toBe('file-modified');
- });
-
- it('returns addition when not a tempFile', () => {
- f.tempFile = true;
-
- expect(vm.iconName).toBe('file-addition');
- });
- });
-
- describe('iconClass', () => {
- it('returns modified when not a tempFile', () => {
- expect(vm.iconClass).toContain('multi-file-modified');
- });
-
- it('returns addition when not a tempFile', () => {
- f.tempFile = true;
-
- expect(vm.iconClass).toContain('multi-file-addition');
- });
- });
- });
-});
diff --git a/spec/javascripts/repo/components/commit_sidebar/list_spec.js b/spec/javascripts/repo/components/commit_sidebar/list_spec.js
deleted file mode 100644
index cb5240ad118..00000000000
--- a/spec/javascripts/repo/components/commit_sidebar/list_spec.js
+++ /dev/null
@@ -1,59 +0,0 @@
-import Vue from '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';
-
-describe('Multi-file editor commit sidebar list', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(commitSidebarList);
-
- vm = createComponentWithStore(Component, store, {
- title: 'Staged',
- fileList: [],
- });
-
- vm.$store.state.rightPanelCollapsed = false;
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- describe('empty file list', () => {
- it('renders no changes text', () => {
- expect(vm.$el.querySelector('.help-block').textContent.trim()).toBe('No changes');
- });
- });
-
- describe('with a list of files', () => {
- beforeEach((done) => {
- const f = file('file name');
- f.changed = true;
- vm.fileList.push(f);
-
- Vue.nextTick(done);
- });
-
- it('renders list', () => {
- expect(vm.$el.querySelectorAll('li').length).toBe(1);
- });
- });
-
- describe('collapsed', () => {
- beforeEach((done) => {
- vm.$store.state.rightPanelCollapsed = true;
-
- Vue.nextTick(done);
- });
-
- it('hides list', () => {
- expect(vm.$el.querySelector('.list-unstyled')).toBeNull();
- expect(vm.$el.querySelector('.help-block')).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/repo/components/ide_context_bar_spec.js b/spec/javascripts/repo/components/ide_context_bar_spec.js
deleted file mode 100644
index 3f8f37d2343..00000000000
--- a/spec/javascripts/repo/components/ide_context_bar_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-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/ide_repo_tree_spec.js b/spec/javascripts/repo/components/ide_repo_tree_spec.js
deleted file mode 100644
index e3bbda514da..00000000000
--- a/spec/javascripts/repo/components/ide_repo_tree_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import ideRepoTree from '~/ide/components/ide_repo_tree.vue';
-import { file, resetStore } from '../helpers';
-
-describe('IdeRepoTree', () => {
- let vm;
-
- beforeEach(() => {
- const IdeRepoTree = Vue.extend(ideRepoTree);
-
- vm = new IdeRepoTree({
- store,
- propsData: {
- treeId: 'abcproject/mybranch',
- },
- });
-
- vm.$store.state.currentBranch = 'master';
- vm.$store.state.isRoot = true;
- vm.$store.state.trees['abcproject/mybranch'] = {
- tree: [file()],
- };
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders a sidebar', () => {
- const tbody = vm.$el.querySelector('tbody');
-
- expect(vm.$el.classList.contains('sidebar-mini')).toBeFalsy();
- expect(tbody.querySelector('.repo-file-options')).toBeFalsy();
- expect(tbody.querySelector('.prev-directory')).toBeFalsy();
- expect(tbody.querySelector('.loading-file')).toBeFalsy();
- expect(tbody.querySelector('.file')).toBeTruthy();
- });
-
- it('renders 3 loading files if tree is loading', (done) => {
- vm.treeId = '123';
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelectorAll('.multi-file-loading-container').length).toEqual(3);
-
- done();
- });
- });
-
- it('renders a prev directory if is not root', (done) => {
- vm.$store.state.isRoot = false;
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('tbody .prev-directory')).toBeTruthy();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/repo/components/ide_side_bar_spec.js b/spec/javascripts/repo/components/ide_side_bar_spec.js
deleted file mode 100644
index 30e45169205..00000000000
--- a/spec/javascripts/repo/components/ide_side_bar_spec.js
+++ /dev/null
@@ -1,43 +0,0 @@
-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/ide_spec.js b/spec/javascripts/repo/components/ide_spec.js
deleted file mode 100644
index acfd63eb8de..00000000000
--- a/spec/javascripts/repo/components/ide_spec.js
+++ /dev/null
@@ -1,39 +0,0 @@
-import Vue from '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('ide component', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(ide);
-
- vm = createComponentWithStore(Component, store, {
- emptyStateSvgPath: 'svg',
- }).$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('does not render panel right when no files open', () => {
- expect(vm.$el.querySelector('.panel-right')).toBeNull();
- });
-
- it('renders panel right when files are open', (done) => {
- vm.$store.state.trees['abcproject/mybranch'] = {
- tree: [file()],
- };
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.panel-right')).toBeNull();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/repo/components/new_branch_form_spec.js b/spec/javascripts/repo/components/new_branch_form_spec.js
deleted file mode 100644
index cd1d073ec18..00000000000
--- a/spec/javascripts/repo/components/new_branch_form_spec.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import Vue from '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';
-
-describe('Multi-file editor new branch form', () => {
- let vm;
-
- beforeEach(() => {
- const Component = Vue.extend(newBranchForm);
-
- vm = createComponentWithStore(Component, store);
-
- vm.$store.state.currentBranch = 'master';
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- describe('template', () => {
- it('renders submit as disabled', () => {
- expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBe('disabled');
- });
-
- it('enables the submit button when branch is not empty', (done) => {
- vm.branchName = 'testing';
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.btn').getAttribute('disabled')).toBeNull();
-
- done();
- });
- });
-
- it('displays current branch creating from', (done) => {
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('p').textContent.replace(/\s+/g, ' ').trim()).toBe('Create from: master');
-
- done();
- });
- });
- });
-
- describe('submitNewBranch', () => {
- beforeEach(() => {
- spyOn(vm, 'createNewBranch').and.returnValue(Promise.resolve());
- });
-
- it('sets to loading', () => {
- vm.submitNewBranch();
-
- expect(vm.loading).toBeTruthy();
- });
-
- it('hides current flash element', (done) => {
- vm.$refs.flashContainer.innerHTML = '<div class="flash-alert"></div>';
-
- vm.submitNewBranch();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.flash-alert')).toBeNull();
-
- done();
- });
- });
-
- it('calls createdNewBranch with branchName', () => {
- vm.branchName = 'testing';
-
- vm.submitNewBranch();
-
- expect(vm.createNewBranch).toHaveBeenCalledWith('testing');
- });
- });
-
- describe('submitNewBranch with error', () => {
- beforeEach(() => {
- spyOn(vm, 'createNewBranch').and.returnValue(Promise.reject({
- json: () => Promise.resolve({
- message: 'error message',
- }),
- }));
- });
-
- it('sets loading to false', (done) => {
- vm.loading = true;
-
- vm.submitNewBranch();
-
- setTimeout(() => {
- expect(vm.loading).toBeFalsy();
-
- done();
- });
- });
-
- it('creates flash element', (done) => {
- vm.submitNewBranch();
-
- setTimeout(() => {
- expect(vm.$el.querySelector('.flash-alert')).not.toBeNull();
- expect(vm.$el.querySelector('.flash-alert').textContent.trim()).toBe('error message');
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/repo/components/new_dropdown/index_spec.js b/spec/javascripts/repo/components/new_dropdown/index_spec.js
deleted file mode 100644
index 6efbbf6d75e..00000000000
--- a/spec/javascripts/repo/components/new_dropdown/index_spec.js
+++ /dev/null
@@ -1,77 +0,0 @@
-import Vue from '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';
-
-describe('new dropdown component', () => {
- let vm;
-
- beforeEach(() => {
- const component = Vue.extend(newDropdown);
-
- vm = createComponentWithStore(component, store, {
- branch: 'master',
- path: '',
- });
-
- vm.$store.state.currentProjectId = 'abcproject';
- vm.$store.state.path = '';
-
- vm.$mount();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- 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('Upload file');
- expect(vm.$el.querySelectorAll('a')[2].textContent.trim()).toBe('New directory');
- });
-
- describe('createNewItem', () => {
- it('sets modalType to blob when new file is clicked', () => {
- vm.$el.querySelectorAll('a')[0].click();
-
- expect(vm.modalType).toBe('blob');
- });
-
- it('sets modalType to tree when new directory is clicked', () => {
- vm.$el.querySelectorAll('a')[2].click();
-
- expect(vm.modalType).toBe('tree');
- });
-
- it('opens modal when link is clicked', (done) => {
- vm.$el.querySelectorAll('a')[0].click();
-
- Vue.nextTick(() => {
- expect(vm.$el.querySelector('.modal')).not.toBeNull();
-
- done();
- });
- });
- });
-
- describe('hideModal', () => {
- beforeAll((done) => {
- vm.openModal = true;
- Vue.nextTick(done);
- });
-
- it('closes modal after toggling', (done) => {
- vm.hideModal();
-
- Vue.nextTick()
- .then(() => {
- expect(vm.$el.querySelector('.modal')).toBeNull();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/repo/components/new_dropdown/modal_spec.js b/spec/javascripts/repo/components/new_dropdown/modal_spec.js
deleted file mode 100644
index 8bbc3100357..00000000000
--- a/spec/javascripts/repo/components/new_dropdown/modal_spec.js
+++ /dev/null
@@ -1,237 +0,0 @@
-import Vue from '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({
- data: {
- 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();
-
- resetStore(vm.$store);
- });
-
- ['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: '',
- parent: projectTree,
- });
-
- vm.entryName = 'testing';
-
- vm.$mount();
- });
-
- it(`sets modal title as ${type}`, () => {
- const title = type === 'tree' ? 'directory' : 'file';
-
- expect(vm.$el.querySelector('.modal-title').textContent.trim()).toBe(`Create new ${title}`);
- });
-
- it(`sets button label as ${type}`, () => {
- const title = type === 'tree' ? 'directory' : 'file';
-
- expect(vm.$el.querySelector('.btn-success').textContent.trim()).toBe(`Create ${title}`);
- });
-
- it(`sets form label as ${type}`, () => {
- const title = type === 'tree' ? 'Directory' : 'File';
-
- expect(vm.$el.querySelector('.label-light').textContent.trim()).toBe(`${title} name`);
- });
-
- describe('createEntryInStore', () => {
- it('calls createTempEntry', () => {
- spyOn(vm, 'createTempEntry');
-
- vm.createEntryInStore();
-
- expect(vm.createTempEntry).toHaveBeenCalledWith({
- projectId: 'abcproject',
- branchId: 'master',
- parent: projectTree,
- name: 'testing',
- type,
- });
- });
-
- it('sets editMode to true', (done) => {
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(vm.$store.state.editMode).toBeTruthy();
-
- done();
- });
- });
-
- it('toggles blob view', (done) => {
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(vm.$store.state.currentBlobView).toBe('repo-editor');
-
- done();
- });
- });
-
- it('opens newly created file', (done) => {
- if (type === 'blob') {
- vm.createEntryInStore();
-
- 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') {
- it('creates new file', (done) => {
- vm.createEntryInStore();
-
- setTimeout(() => {
- 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) => {
- const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
- baseTree.push(file('testing', '1', type));
-
- vm.createEntryInStore();
-
- setTimeout(() => {
- expect(baseTree.length).toBe(1);
- expect(baseTree[0].name).toBe('testing');
- expect(baseTree[0].type).toBe('blob');
- expect(baseTree[0].tempFile).toBeFalsy();
-
- done();
- });
- });
- } else {
- it('creates new tree', () => {
- vm.createEntryInStore();
-
- 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();
-
- 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', () => {
- const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
- baseTree.push(file('app', '1', 'tree'));
-
- vm.entryName = 'app/test';
- vm.createEntryInStore();
-
- 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', () => {
- const baseTree = vm.$store.state.trees['abcproject/mybranch'].tree;
- baseTree.push(file('app', '1', 'tree'));
-
- vm.entryName = 'app';
- vm.createEntryInStore();
-
- expect(baseTree.length).toBe(1);
- expect(baseTree[0].name).toBe('app');
- expect(baseTree[0].tempFile).toBeFalsy();
- expect(baseTree[0].tree.length).toBe(0);
- });
- }
- });
- });
- });
-
- it('focuses field on mount', () => {
- document.body.innerHTML += '<div class="js-test"></div>';
-
- vm = createComponentWithStore(Component, store, {
- type: 'tree',
- projectId: 'abcproject',
- branchId: 'master',
- path: '',
- }).$mount('.js-test');
-
- expect(document.activeElement).toBe(vm.$refs.fieldName);
-
- vm.$el.remove();
- });
-});
diff --git a/spec/javascripts/repo/components/new_dropdown/upload_spec.js b/spec/javascripts/repo/components/new_dropdown/upload_spec.js
deleted file mode 100644
index 667112ab21a..00000000000
--- a/spec/javascripts/repo/components/new_dropdown/upload_spec.js
+++ /dev/null
@@ -1,158 +0,0 @@
-import Vue from 'vue';
-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({
- data: {
- 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();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- describe('readFile', () => {
- beforeEach(() => {
- spyOn(FileReader.prototype, 'readAsText');
- spyOn(FileReader.prototype, 'readAsDataURL');
- });
-
- it('calls readAsText for text files', () => {
- const file = {
- type: 'text/html',
- };
-
- vm.readFile(file);
-
- expect(FileReader.prototype.readAsText).toHaveBeenCalledWith(file);
- });
-
- it('calls readAsDataURL for non-text files', () => {
- const file = {
- type: 'images/png',
- };
-
- vm.readFile(file);
-
- expect(FileReader.prototype.readAsDataURL).toHaveBeenCalledWith(file);
- });
- });
-
- describe('createFile', () => {
- const target = {
- result: 'content',
- };
- const binaryTarget = {
- result: 'base64,base64content',
- };
- const file = {
- name: 'file',
- };
-
- it('creates new file', (done) => {
- vm.createFile(target, file, true);
-
- vm.$nextTick(() => {
- 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) => {
- 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(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();
- });
- });
-
- it('splits content on base64 if binary', (done) => {
- vm.createFile(binaryTarget, file, false);
-
- vm.$nextTick(() => {
- 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
deleted file mode 100644
index 93e94b4f24c..00000000000
--- a/spec/javascripts/repo/components/repo_commit_section_spec.js
+++ /dev/null
@@ -1,140 +0,0 @@
-import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
-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';
-
-describe('RepoCommitSection', () => {
- let vm;
-
- function createComponent() {
- const RepoCommitSection = Vue.extend(repoCommitSection);
-
- const comp = new 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('file1'), file('file2')];
- comp.$store.state.openFiles.forEach(f => Object.assign(f, {
- changed: true,
- content: 'testing',
- }));
-
- return comp.$mount();
- }
-
- beforeEach((done) => {
- vm = createComponent();
-
- 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);
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders a commit section', () => {
- const changedFileElements = [...vm.$el.querySelectorAll('.multi-file-commit-list li')];
- const submitCommit = vm.$el.querySelector('form .btn');
-
- expect(vm.$el.querySelector('.multi-file-commit-form')).not.toBeNull();
- expect(changedFileElements.length).toEqual(2);
-
- changedFileElements.forEach((changedFile, i) => {
- expect(changedFile.textContent.trim()).toEqual(vm.$store.getters.changedFiles[i].path);
- });
-
- expect(submitCommit.disabled).toBeTruthy();
- expect(submitCommit.querySelector('.fa-spinner.fa-spin')).toBeNull();
- });
-
- describe('when submitting', () => {
- let changedFiles;
-
- beforeEach(() => {
- vm.commitMessage = 'testing';
- changedFiles = JSON.parse(JSON.stringify(vm.$store.getters.changedFiles));
-
- spyOn(service, 'commit').and.returnValue(Promise.resolve({
- data: {
- short_id: '1',
- stats: {},
- },
- }));
- });
-
- it('allows you to submit', () => {
- expect(vm.$el.querySelector('form .btn').disabled).toBeTruthy();
- });
-
- it('submits commit', (done) => {
- vm.makeCommit();
-
- // Wait for the branch check to finish
- getSetTimeoutPromise()
- .then(() => Vue.nextTick())
- .then(() => {
- const args = service.commit.calls.allArgs()[0];
- const { commit_message, actions, branch: payloadBranch } = args[1];
-
- expect(commit_message).toBe('testing');
- expect(actions.length).toEqual(2);
- expect(payloadBranch).toEqual('master');
- expect(actions[0].action).toEqual('update');
- expect(actions[1].action).toEqual('update');
- expect(actions[0].content).toEqual(changedFiles[0].content);
- expect(actions[1].content).toEqual(changedFiles[1].content);
- expect(actions[0].file_path).toEqual(changedFiles[0].path);
- expect(actions[1].file_path).toEqual(changedFiles[1].path);
- })
- .then(done)
- .catch(done.fail);
- });
-
- it('redirects to MR creation page if start new MR checkbox checked', (done) => {
- spyOn(urlUtils, 'visitUrl');
- vm.startNewMR = true;
-
- vm.makeCommit();
-
- getSetTimeoutPromise()
- .then(() => Vue.nextTick())
- .then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalled();
- })
- .then(done)
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_edit_button_spec.js b/spec/javascripts/repo/components/repo_edit_button_spec.js
deleted file mode 100644
index 2895b794506..00000000000
--- a/spec/javascripts/repo/components/repo_edit_button_spec.js
+++ /dev/null
@@ -1,83 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoEditButton from '~/ide/components/repo_edit_button.vue';
-import { file, resetStore } from '../helpers';
-
-describe('RepoEditButton', () => {
- let vm;
-
- beforeEach(() => {
- const f = file();
- const RepoEditButton = Vue.extend(repoEditButton);
-
- vm = new RepoEditButton({
- store,
- });
-
- f.active = true;
- vm.$store.dispatch('setInitialData', {
- canCommit: true,
- onTopOfBranch: true,
- });
- vm.$store.state.openFiles.push(f);
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders an edit button', () => {
- vm.$mount();
-
- expect(vm.$el.querySelector('.btn')).not.toBeNull();
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
- });
-
- it('renders edit button with cancel text', () => {
- vm.$store.state.editMode = true;
-
- vm.$mount();
-
- expect(vm.$el.querySelector('.btn')).not.toBeNull();
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Cancel edit');
- });
-
- it('toggles edit mode on click', (done) => {
- vm.$mount();
-
- vm.$el.querySelector('.btn').click();
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.btn').textContent.trim()).toBe('Edit');
-
- done();
- });
- });
-
- describe('discardPopupOpen', () => {
- beforeEach(() => {
- vm.$store.state.discardPopupOpen = true;
- vm.$store.state.editMode = true;
- vm.$store.state.openFiles[0].changed = true;
-
- vm.$mount();
- });
-
- it('renders popup', () => {
- expect(vm.$el.querySelector('.modal')).not.toBeNull();
- });
-
- it('removes all changed files', (done) => {
- vm.$el.querySelector('.btn-warning').click();
-
- vm.$nextTick(() => {
- expect(vm.$store.getters.changedFiles.length).toBe(0);
- expect(vm.$el.querySelector('.modal')).toBeNull();
-
- done();
- });
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_editor_spec.js b/spec/javascripts/repo/components/repo_editor_spec.js
deleted file mode 100644
index e7b2ed08acd..00000000000
--- a/spec/javascripts/repo/components/repo_editor_spec.js
+++ /dev/null
@@ -1,60 +0,0 @@
-import Vue from 'vue';
-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', () => {
- let vm;
-
- beforeEach((done) => {
- const f = file();
- const RepoEditor = Vue.extend(repoEditor);
-
- vm = new RepoEditor({
- store,
- });
-
- f.active = true;
- f.tempFile = true;
- vm.$store.state.openFiles.push(f);
- vm.$store.getters.activeFile.html = 'testing';
- vm.monaco = true;
-
- vm.$mount();
-
- monacoLoader(['vs/editor/editor.main'], () => {
- setTimeout(done, 0);
- });
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders an ide container', (done) => {
- Vue.nextTick(() => {
- expect(vm.shouldHideEditor).toBeFalsy();
-
- done();
- });
- });
-
- describe('when open file is binary and not raw', () => {
- beforeEach((done) => {
- vm.$store.getters.activeFile.binary = true;
-
- Vue.nextTick(done);
- });
-
- it('does not render the IDE', () => {
- expect(vm.shouldHideEditor).toBeTruthy();
- });
-
- it('shows activeFile html', () => {
- expect(vm.$el.textContent).toContain('testing');
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_file_buttons_spec.js b/spec/javascripts/repo/components/repo_file_buttons_spec.js
deleted file mode 100644
index 115569a9117..00000000000
--- a/spec/javascripts/repo/components/repo_file_buttons_spec.js
+++ /dev/null
@@ -1,49 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoFileButtons from '~/ide/components/repo_file_buttons.vue';
-import { file, resetStore } from '../helpers';
-
-describe('RepoFileButtons', () => {
- const activeFile = file();
- let vm;
-
- function createComponent() {
- const RepoFileButtons = Vue.extend(repoFileButtons);
-
- activeFile.rawPath = 'test';
- activeFile.blamePath = 'test';
- activeFile.commitsPath = 'test';
- activeFile.active = true;
- store.state.openFiles.push(activeFile);
-
- return new RepoFileButtons({
- store,
- }).$mount();
- }
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders Raw, Blame, History, Permalink and Preview toggle', (done) => {
- vm = createComponent();
-
- vm.$nextTick(() => {
- const raw = vm.$el.querySelector('.raw');
- const blame = vm.$el.querySelector('.blame');
- const history = vm.$el.querySelector('.history');
-
- expect(raw.href).toMatch(`/${activeFile.rawPath}`);
- expect(raw.textContent.trim()).toEqual('Raw');
- expect(blame.href).toMatch(`/${activeFile.blamePath}`);
- expect(blame.textContent.trim()).toEqual('Blame');
- expect(history.href).toMatch(`/${activeFile.commitsPath}`);
- expect(history.textContent.trim()).toEqual('History');
- expect(vm.$el.querySelector('.permalink').textContent.trim()).toEqual('Permalink');
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_file_spec.js b/spec/javascripts/repo/components/repo_file_spec.js
deleted file mode 100644
index 27b55ed1f87..00000000000
--- a/spec/javascripts/repo/components/repo_file_spec.js
+++ /dev/null
@@ -1,98 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoFile from '~/ide/components/repo_file.vue';
-import { file, resetStore } from '../helpers';
-
-describe('RepoFile', () => {
- const updated = 'updated';
- let vm;
-
- function createComponent(propsData) {
- const RepoFile = Vue.extend(repoFile);
-
- return new RepoFile({
- store,
- propsData,
- }).$mount();
- }
-
- afterEach(() => {
- resetStore(vm.$store);
- });
-
- it('renders link, icon and name', () => {
- const RepoFile = Vue.extend(repoFile);
- vm = new RepoFile({
- store,
- propsData: {
- file: file('t4'),
- },
- });
- spyOn(vm, 'timeFormated').and.returnValue(updated);
- vm.$mount();
-
- const name = vm.$el.querySelector('.repo-file-name');
-
- expect(name.href).toMatch('');
- expect(name.textContent.trim()).toEqual(vm.file.name);
- });
-
- it('does render if hasFiles is true and is loading tree', () => {
- vm = createComponent({
- file: file('t1'),
- });
-
- expect(vm.$el.querySelector('.fa-spin.fa-spinner')).toBeFalsy();
- });
-
- it('does not render commit message and datetime if mini', (done) => {
- vm = createComponent({
- file: file('t2'),
- });
- vm.$store.state.openFiles.push(vm.file);
-
- vm.$nextTick(() => {
- expect(vm.$el.querySelector('.commit-message')).toBeFalsy();
- expect(vm.$el.querySelector('.commit-update')).toBeFalsy();
-
- done();
- });
- });
-
- it('fires clickFile when the link is clicked', () => {
- vm = createComponent({
- file: file('t3'),
- });
-
- spyOn(vm, 'clickFile');
-
- vm.$el.click();
-
- expect(vm.clickFile).toHaveBeenCalledWith(vm.file);
- });
-
- describe('submodule', () => {
- let f;
-
- beforeEach(() => {
- f = file('submodule name', '123456789');
- f.type = 'submodule';
-
- vm = createComponent({
- file: f,
- });
- });
-
- afterEach(() => {
- vm.$destroy();
- });
-
- it('renders submodule short ID', () => {
- expect(vm.$el.querySelector('.commit-sha').textContent.trim()).toBe('12345678');
- });
-
- it('renders ID next to submodule name', () => {
- expect(vm.$el.querySelector('td').textContent.replace(/\s+/g, ' ')).toContain('submodule name @ 12345678');
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_loading_file_spec.js b/spec/javascripts/repo/components/repo_loading_file_spec.js
deleted file mode 100644
index 18366fb89bc..00000000000
--- a/spec/javascripts/repo/components/repo_loading_file_spec.js
+++ /dev/null
@@ -1,63 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoLoadingFile from '~/ide/components/repo_loading_file.vue';
-import { resetStore } from '../helpers';
-
-describe('RepoLoadingFile', () => {
- let vm;
-
- function createComponent() {
- const RepoLoadingFile = Vue.extend(repoLoadingFile);
-
- return new RepoLoadingFile({
- store,
- }).$mount();
- }
-
- function assertLines(lines) {
- lines.forEach((line, n) => {
- const index = n + 1;
- expect(line.classList.contains(`skeleton-line-${index}`)).toBeTruthy();
- });
- }
-
- function assertColumns(columns) {
- columns.forEach((column) => {
- const container = column.querySelector('.animation-container');
- const lines = [...container.querySelectorAll(':scope > div')];
-
- expect(container).toBeTruthy();
- expect(lines.length).toEqual(6);
- assertLines(lines);
- });
- }
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders 3 columns of animated LoC', () => {
- vm = createComponent();
- const columns = [...vm.$el.querySelectorAll('td')];
-
- expect(columns.length).toEqual(3);
- assertColumns(columns);
- });
-
- 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(() => {
- const columns = [...vm.$el.querySelectorAll('td')];
-
- expect(columns.length).toEqual(1);
- assertColumns(columns);
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_prev_directory_spec.js b/spec/javascripts/repo/components/repo_prev_directory_spec.js
deleted file mode 100644
index ff26cab2262..00000000000
--- a/spec/javascripts/repo/components/repo_prev_directory_spec.js
+++ /dev/null
@@ -1,45 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoPrevDirectory from '~/ide/components/repo_prev_directory.vue';
-import { resetStore } from '../helpers';
-
-describe('RepoPrevDirectory', () => {
- let vm;
- const parentLink = 'parent';
- function createComponent() {
- const RepoPrevDirectory = Vue.extend(repoPrevDirectory);
-
- const comp = new RepoPrevDirectory({
- store,
- });
-
- comp.$store.state.parentTreeUrl = parentLink;
-
- return comp.$mount();
- }
-
- beforeEach(() => {
- vm = createComponent();
- });
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders a prev dir link', () => {
- const link = vm.$el.querySelector('a');
-
- expect(link.href).toMatch(`/${parentLink}`);
- expect(link.textContent).toEqual('...');
- });
-
- it('clicking row triggers getTreeData', () => {
- spyOn(vm, 'getTreeData');
-
- vm.$el.querySelector('td').click();
-
- expect(vm.getTreeData).toHaveBeenCalledWith({ endpoint: parentLink });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_preview_spec.js b/spec/javascripts/repo/components/repo_preview_spec.js
deleted file mode 100644
index e90837e4cb2..00000000000
--- a/spec/javascripts/repo/components/repo_preview_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoPreview from '~/ide/components/repo_preview.vue';
-import { file, resetStore } from '../helpers';
-
-describe('RepoPreview', () => {
- let vm;
-
- function createComponent() {
- const f = file();
- const RepoPreview = Vue.extend(repoPreview);
-
- const comp = new RepoPreview({
- store,
- });
-
- f.active = true;
- f.html = 'test';
-
- comp.$store.state.openFiles.push(f);
-
- return comp.$mount();
- }
-
- afterEach(() => {
- vm.$destroy();
-
- resetStore(vm.$store);
- });
-
- it('renders a div with the activeFile html', () => {
- vm = createComponent();
-
- expect(vm.$el.tagName).toEqual('DIV');
- expect(vm.$el.innerHTML).toContain('test');
- });
-});
diff --git a/spec/javascripts/repo/components/repo_tab_spec.js b/spec/javascripts/repo/components/repo_tab_spec.js
deleted file mode 100644
index 933e8d3a06a..00000000000
--- a/spec/javascripts/repo/components/repo_tab_spec.js
+++ /dev/null
@@ -1,108 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoTab from '~/ide/components/repo_tab.vue';
-import { file, resetStore } from '../helpers';
-
-describe('RepoTab', () => {
- let vm;
-
- function createComponent(propsData) {
- const RepoTab = Vue.extend(repoTab);
-
- return new RepoTab({
- store,
- propsData,
- }).$mount();
- }
-
- afterEach(() => {
- resetStore(vm.$store);
- });
-
- it('renders a close link and a name link', () => {
- vm = createComponent({
- tab: file(),
- });
- vm.$store.state.openFiles.push(vm.tab);
- const close = vm.$el.querySelector('.multi-file-tab-close');
- const name = vm.$el.querySelector(`[title="${vm.tab.url}"]`);
-
- expect(close.querySelector('.fa-times')).toBeTruthy();
- expect(name.textContent.trim()).toEqual(vm.tab.name);
- });
-
- it('fires clickFile when the link is clicked', () => {
- vm = createComponent({
- tab: file(),
- });
-
- spyOn(vm, 'clickFile');
-
- vm.$el.click();
-
- expect(vm.clickFile).toHaveBeenCalledWith(vm.tab);
- });
-
- it('calls closeFile when clicking close button', () => {
- vm = createComponent({
- tab: file(),
- });
-
- spyOn(vm, 'closeFile');
-
- vm.$el.querySelector('.multi-file-tab-close').click();
-
- expect(vm.closeFile).toHaveBeenCalledWith({ file: vm.tab });
- });
-
- it('renders an fa-circle icon if tab is changed', () => {
- const tab = file('changedFile');
- tab.changed = true;
- vm = createComponent({
- tab,
- });
-
- expect(vm.$el.querySelector('.multi-file-tab-close .fa-circle')).not.toBeNull();
- });
-
- describe('methods', () => {
- describe('closeTab', () => {
- it('does not close tab if is changed', (done) => {
- const tab = file('closeFile');
- tab.changed = true;
- tab.opened = true;
- vm = createComponent({
- tab,
- });
- vm.$store.state.openFiles.push(tab);
- vm.$store.dispatch('setFileActive', tab);
-
- vm.$el.querySelector('.multi-file-tab-close').click();
-
- vm.$nextTick(() => {
- expect(tab.opened).toBeTruthy();
-
- done();
- });
- });
-
- it('closes tab when clicking close btn', (done) => {
- const tab = file('lose');
- tab.opened = true;
- vm = createComponent({
- tab,
- });
- vm.$store.state.openFiles.push(tab);
- vm.$store.dispatch('setFileActive', tab);
-
- vm.$el.querySelector('.multi-file-tab-close').click();
-
- vm.$nextTick(() => {
- expect(tab.opened).toBeFalsy();
-
- done();
- });
- });
- });
- });
-});
diff --git a/spec/javascripts/repo/components/repo_tabs_spec.js b/spec/javascripts/repo/components/repo_tabs_spec.js
deleted file mode 100644
index 2c363364d70..00000000000
--- a/spec/javascripts/repo/components/repo_tabs_spec.js
+++ /dev/null
@@ -1,37 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import repoTabs from '~/ide/components/repo_tabs.vue';
-import { file, resetStore } from '../helpers';
-
-describe('RepoTabs', () => {
- const openedFiles = [file('open1'), file('open2')];
- let vm;
-
- function createComponent() {
- const RepoTabs = Vue.extend(repoTabs);
-
- return new RepoTabs({
- store,
- }).$mount();
- }
-
- afterEach(() => {
- resetStore(vm.$store);
- });
-
- it('renders a list of tabs', (done) => {
- vm = createComponent();
- openedFiles[0].active = true;
- vm.$store.state.openFiles = openedFiles;
-
- vm.$nextTick(() => {
- const tabs = [...vm.$el.querySelectorAll('.multi-file-tab')];
-
- expect(tabs.length).toEqual(2);
- expect(tabs[0].classList.contains('active')).toBeTruthy();
- expect(tabs[1].classList.contains('active')).toBeFalsy();
-
- done();
- });
- });
-});
diff --git a/spec/javascripts/repo/helpers.js b/spec/javascripts/repo/helpers.js
deleted file mode 100644
index ac43d221198..00000000000
--- a/spec/javascripts/repo/helpers.js
+++ /dev/null
@@ -1,16 +0,0 @@
-import { decorateData } from '~/ide/stores/utils';
-import state from '~/ide/stores/state';
-
-export const resetStore = (store) => {
- store.replaceState(state());
-};
-
-export const file = (name = 'name', id = name, type = '') => decorateData({
- id,
- type,
- icon: 'icon',
- 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
deleted file mode 100644
index af12ca15369..00000000000
--- a/spec/javascripts/repo/lib/common/disposable_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import Disposable from '~/ide/lib/common/disposable';
-
-describe('Multi-file editor library disposable class', () => {
- let instance;
- let disposableClass;
-
- beforeEach(() => {
- instance = new Disposable();
-
- disposableClass = {
- dispose: jasmine.createSpy('dispose'),
- };
- });
-
- afterEach(() => {
- instance.dispose();
- });
-
- describe('add', () => {
- it('adds disposable classes', () => {
- instance.add(disposableClass);
-
- expect(instance.disposers.size).toBe(1);
- });
- });
-
- describe('dispose', () => {
- beforeEach(() => {
- instance.add(disposableClass);
- });
-
- it('calls dispose on all cached disposers', () => {
- instance.dispose();
-
- expect(disposableClass.dispose).toHaveBeenCalled();
- });
-
- it('clears cached disposers', () => {
- instance.dispose();
-
- expect(instance.disposers.size).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/repo/lib/common/model_manager_spec.js b/spec/javascripts/repo/lib/common/model_manager_spec.js
deleted file mode 100644
index 563c2e33834..00000000000
--- a/spec/javascripts/repo/lib/common/model_manager_spec.js
+++ /dev/null
@@ -1,81 +0,0 @@
-/* global monaco */
-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', () => {
- let instance;
-
- beforeEach((done) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- instance = new ModelManager(monaco);
-
- done();
- });
- });
-
- afterEach(() => {
- instance.dispose();
- });
-
- describe('addModel', () => {
- it('caches model', () => {
- instance.addModel(file());
-
- expect(instance.models.size).toBe(1);
- });
-
- it('caches model by file path', () => {
- instance.addModel(file('path-name'));
-
- expect(instance.models.keys().next().value).toBe('path-name');
- });
-
- it('adds model into disposable', () => {
- spyOn(instance.disposable, 'add').and.callThrough();
-
- instance.addModel(file());
-
- expect(instance.disposable.add).toHaveBeenCalled();
- });
-
- it('returns cached model', () => {
- spyOn(instance.models, 'get').and.callThrough();
-
- instance.addModel(file());
- instance.addModel(file());
-
- expect(instance.models.get).toHaveBeenCalled();
- });
- });
-
- describe('hasCachedModel', () => {
- it('returns false when no models exist', () => {
- expect(instance.hasCachedModel('path')).toBeFalsy();
- });
-
- it('returns true when model exists', () => {
- instance.addModel(file('path-name'));
-
- expect(instance.hasCachedModel('path-name')).toBeTruthy();
- });
- });
-
- describe('dispose', () => {
- it('clears cached models', () => {
- instance.addModel(file());
-
- instance.dispose();
-
- expect(instance.models.size).toBe(0);
- });
-
- it('calls disposable dispose', () => {
- spyOn(instance.disposable, 'dispose').and.callThrough();
-
- instance.dispose();
-
- expect(instance.disposable.dispose).toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/javascripts/repo/lib/common/model_spec.js b/spec/javascripts/repo/lib/common/model_spec.js
deleted file mode 100644
index 878a4a3f3fe..00000000000
--- a/spec/javascripts/repo/lib/common/model_spec.js
+++ /dev/null
@@ -1,84 +0,0 @@
-/* global monaco */
-import monacoLoader from '~/ide/monaco_loader';
-import Model from '~/ide/lib/common/model';
-import { file } from '../../helpers';
-
-describe('Multi-file editor library model', () => {
- let model;
-
- beforeEach((done) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- model = new Model(monaco, file('path'));
-
- done();
- });
- });
-
- afterEach(() => {
- model.dispose();
- });
-
- it('creates original model & new model', () => {
- expect(model.originalModel).not.toBeNull();
- expect(model.model).not.toBeNull();
- });
-
- describe('path', () => {
- it('returns file path', () => {
- expect(model.path).toBe('path');
- });
- });
-
- describe('getModel', () => {
- it('returns model', () => {
- expect(model.getModel()).toBe(model.model);
- });
- });
-
- describe('getOriginalModel', () => {
- it('returns original model', () => {
- expect(model.getOriginalModel()).toBe(model.originalModel);
- });
- });
-
- describe('onChange', () => {
- it('caches event by path', () => {
- model.onChange(() => {});
-
- expect(model.events.size).toBe(1);
- expect(model.events.keys().next().value).toBe('path');
- });
-
- it('calls callback on change', (done) => {
- const spy = jasmine.createSpy();
- model.onChange(spy);
-
- model.getModel().setValue('123');
-
- setTimeout(() => {
- expect(spy).toHaveBeenCalledWith(model.getModel(), jasmine.anything());
- done();
- });
- });
- });
-
- describe('dispose', () => {
- it('calls disposable dispose', () => {
- spyOn(model.disposable, 'dispose').and.callThrough();
-
- model.dispose();
-
- expect(model.disposable.dispose).toHaveBeenCalled();
- });
-
- it('clears events', () => {
- model.onChange(() => {});
-
- expect(model.events.size).toBe(1);
-
- model.dispose();
-
- expect(model.events.size).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/repo/lib/decorations/controller_spec.js b/spec/javascripts/repo/lib/decorations/controller_spec.js
deleted file mode 100644
index fea12d74dca..00000000000
--- a/spec/javascripts/repo/lib/decorations/controller_spec.js
+++ /dev/null
@@ -1,120 +0,0 @@
-/* global monaco */
-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', () => {
- let editorInstance;
- let controller;
- let model;
-
- beforeEach((done) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- editorInstance = editor.create(monaco);
- editorInstance.createInstance(document.createElement('div'));
-
- controller = new DecorationsController(editorInstance);
- model = new Model(monaco, file('path'));
-
- done();
- });
- });
-
- afterEach(() => {
- model.dispose();
- editorInstance.dispose();
- controller.dispose();
- });
-
- describe('getAllDecorationsForModel', () => {
- it('returns empty array when no decorations exist for model', () => {
- const decorations = controller.getAllDecorationsForModel(model);
-
- expect(decorations).toEqual([]);
- });
-
- it('returns decorations by model URL', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- const decorations = controller.getAllDecorationsForModel(model);
-
- expect(decorations[0]).toEqual({ decoration: 'decorationValue' });
- });
- });
-
- describe('addDecorations', () => {
- it('caches decorations in a new map', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.decorations.size).toBe(1);
- });
-
- it('does not create new cache model', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue2' }]);
-
- expect(controller.decorations.size).toBe(1);
- });
-
- it('caches decorations by model URL', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.decorations.size).toBe(1);
- expect(controller.decorations.keys().next().value).toBe('path');
- });
-
- it('calls decorate method', () => {
- spyOn(controller, 'decorate');
-
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- expect(controller.decorate).toHaveBeenCalled();
- });
- });
-
- describe('decorate', () => {
- it('sets decorations on editor instance', () => {
- spyOn(controller.editor.instance, 'deltaDecorations');
-
- controller.decorate(model);
-
- expect(controller.editor.instance.deltaDecorations).toHaveBeenCalledWith([], []);
- });
-
- it('caches decorations', () => {
- spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
-
- controller.decorate(model);
-
- expect(controller.editorDecorations.size).toBe(1);
- });
-
- it('caches decorations by model URL', () => {
- spyOn(controller.editor.instance, 'deltaDecorations').and.returnValue([]);
-
- controller.decorate(model);
-
- expect(controller.editorDecorations.keys().next().value).toBe('path');
- });
- });
-
- describe('dispose', () => {
- it('clears cached decorations', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- controller.dispose();
-
- expect(controller.decorations.size).toBe(0);
- });
-
- it('clears cached editorDecorations', () => {
- controller.addDecorations(model, 'key', [{ decoration: 'decorationValue' }]);
-
- controller.dispose();
-
- expect(controller.editorDecorations.size).toBe(0);
- });
- });
-});
diff --git a/spec/javascripts/repo/lib/diff/controller_spec.js b/spec/javascripts/repo/lib/diff/controller_spec.js
deleted file mode 100644
index 1d55c165260..00000000000
--- a/spec/javascripts/repo/lib/diff/controller_spec.js
+++ /dev/null
@@ -1,176 +0,0 @@
-/* global monaco */
-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', () => {
- let editorInstance;
- let controller;
- let modelManager;
- let decorationsController;
- let model;
-
- beforeEach((done) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- editorInstance = editor.create(monaco);
- editorInstance.createInstance(document.createElement('div'));
-
- modelManager = new ModelManager(monaco);
- decorationsController = new DecorationsController(editorInstance);
-
- model = modelManager.addModel(file());
-
- controller = new DirtyDiffController(modelManager, decorationsController);
-
- done();
- });
- });
-
- afterEach(() => {
- controller.dispose();
- model.dispose();
- decorationsController.dispose();
- editorInstance.dispose();
- });
-
- describe('getDiffChangeType', () => {
- ['added', 'removed', 'modified'].forEach((type) => {
- it(`returns ${type}`, () => {
- const change = {
- [type]: true,
- };
-
- expect(getDiffChangeType(change)).toBe(type);
- });
- });
- });
-
- describe('getDecorator', () => {
- ['added', 'removed', 'modified'].forEach((type) => {
- it(`returns with linesDecorationsClassName for ${type}`, () => {
- const change = {
- [type]: true,
- };
-
- expect(
- getDecorator(change).options.linesDecorationsClassName,
- ).toBe(`dirty-diff dirty-diff-${type}`);
- });
-
- it('returns with line numbers', () => {
- const change = {
- lineNumber: 1,
- endLineNumber: 2,
- [type]: true,
- };
-
- const range = getDecorator(change).range;
-
- expect(range.startLineNumber).toBe(1);
- expect(range.endLineNumber).toBe(2);
- expect(range.startColumn).toBe(1);
- expect(range.endColumn).toBe(1);
- });
- });
- });
-
- describe('attachModel', () => {
- it('adds change event callback', () => {
- spyOn(model, 'onChange');
-
- controller.attachModel(model);
-
- expect(model.onChange).toHaveBeenCalled();
- });
-
- it('calls throttledComputeDiff on change', () => {
- spyOn(controller, 'throttledComputeDiff');
-
- controller.attachModel(model);
-
- model.getModel().setValue('123');
-
- expect(controller.throttledComputeDiff).toHaveBeenCalled();
- });
- });
-
- describe('computeDiff', () => {
- it('posts to worker', () => {
- spyOn(controller.dirtyDiffWorker, 'postMessage');
-
- controller.computeDiff(model);
-
- expect(controller.dirtyDiffWorker.postMessage).toHaveBeenCalledWith({
- path: model.path,
- originalContent: '',
- newContent: '',
- });
- });
- });
-
- describe('reDecorate', () => {
- it('calls decorations controller decorate', () => {
- spyOn(controller.decorationsController, 'decorate');
-
- controller.reDecorate(model);
-
- expect(controller.decorationsController.decorate).toHaveBeenCalledWith(model);
- });
- });
-
- describe('decorate', () => {
- it('adds decorations into decorations controller', () => {
- spyOn(controller.decorationsController, 'addDecorations');
-
- controller.decorate({ data: { changes: [], path: 'path' } });
-
- expect(controller.decorationsController.addDecorations).toHaveBeenCalledWith('path', 'dirtyDiff', jasmine.anything());
- });
-
- it('adds decorations into editor', () => {
- const spy = spyOn(controller.decorationsController.editor.instance, 'deltaDecorations');
-
- controller.decorate({ data: { changes: computeDiff('123', '1234'), path: 'path' } });
-
- expect(spy).toHaveBeenCalledWith([], [{
- range: new monaco.Range(
- 1, 1, 1, 1,
- ),
- options: {
- isWholeLine: true,
- linesDecorationsClassName: 'dirty-diff dirty-diff-modified',
- },
- }]);
- });
- });
-
- describe('dispose', () => {
- it('calls disposable dispose', () => {
- spyOn(controller.disposable, 'dispose').and.callThrough();
-
- controller.dispose();
-
- expect(controller.disposable.dispose).toHaveBeenCalled();
- });
-
- it('terminates worker', () => {
- spyOn(controller.dirtyDiffWorker, 'terminate').and.callThrough();
-
- controller.dispose();
-
- expect(controller.dirtyDiffWorker.terminate).toHaveBeenCalled();
- });
-
- it('removes worker event listener', () => {
- spyOn(controller.dirtyDiffWorker, 'removeEventListener').and.callThrough();
-
- controller.dispose();
-
- expect(controller.dirtyDiffWorker.removeEventListener).toHaveBeenCalledWith('message', jasmine.anything());
- });
- });
-});
diff --git a/spec/javascripts/repo/lib/diff/diff_spec.js b/spec/javascripts/repo/lib/diff/diff_spec.js
deleted file mode 100644
index 57f3ac3d365..00000000000
--- a/spec/javascripts/repo/lib/diff/diff_spec.js
+++ /dev/null
@@ -1,80 +0,0 @@
-import { computeDiff } from '~/ide/lib/diff/diff';
-
-describe('Multi-file editor library diff calculator', () => {
- describe('computeDiff', () => {
- it('returns empty array if no changes', () => {
- const diff = computeDiff('123', '123');
-
- expect(diff).toEqual([]);
- });
-
- describe('modified', () => {
- it('', () => {
- const diff = computeDiff('123', '1234')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeUndefined();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n1234\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeUndefined();
- expect(diff.lineNumber).toBe(2);
- });
- });
-
- describe('added', () => {
- it('', () => {
- const diff = computeDiff('123', '123\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeUndefined();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n123\n1234\n123')[0];
-
- expect(diff.added).toBeTruthy();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeUndefined();
- expect(diff.lineNumber).toBe(3);
- });
- });
-
- describe('removed', () => {
- it('', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.added).toBeUndefined();
- expect(diff.modified).toBeUndefined();
- expect(diff.removed).toBeTruthy();
- });
-
- it('', () => {
- const diff = computeDiff('123\n123\n123', '123\n123')[0];
-
- expect(diff.added).toBeUndefined();
- expect(diff.modified).toBeTruthy();
- expect(diff.removed).toBeTruthy();
- expect(diff.lineNumber).toBe(2);
- });
- });
-
- it('includes line number of change', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.lineNumber).toBe(1);
- });
-
- it('includes end line number of change', () => {
- const diff = computeDiff('123', '')[0];
-
- expect(diff.endLineNumber).toBe(1);
- });
- });
-});
diff --git a/spec/javascripts/repo/lib/editor_options_spec.js b/spec/javascripts/repo/lib/editor_options_spec.js
deleted file mode 100644
index edbf5450dce..00000000000
--- a/spec/javascripts/repo/lib/editor_options_spec.js
+++ /dev/null
@@ -1,7 +0,0 @@
-import editorOptions from '~/ide/lib/editor_options';
-
-describe('Multi-file editor library editor options', () => {
- it('returns an array', () => {
- expect(editorOptions).toEqual(jasmine.any(Array));
- });
-});
diff --git a/spec/javascripts/repo/lib/editor_spec.js b/spec/javascripts/repo/lib/editor_spec.js
deleted file mode 100644
index 8d51d48a782..00000000000
--- a/spec/javascripts/repo/lib/editor_spec.js
+++ /dev/null
@@ -1,128 +0,0 @@
-/* global monaco */
-import monacoLoader from '~/ide/monaco_loader';
-import editor from '~/ide/lib/editor';
-import { file } from '../helpers';
-
-describe('Multi-file editor library', () => {
- let instance;
-
- beforeEach((done) => {
- monacoLoader(['vs/editor/editor.main'], () => {
- instance = editor.create(monaco);
-
- done();
- });
- });
-
- afterEach(() => {
- instance.dispose();
- });
-
- it('creates instance of editor', () => {
- expect(editor.editorInstance).not.toBeNull();
- });
-
- describe('createInstance', () => {
- let el;
-
- beforeEach(() => {
- el = document.createElement('div');
- });
-
- it('creates editor instance', () => {
- spyOn(instance.monaco.editor, 'create').and.callThrough();
-
- instance.createInstance(el);
-
- expect(instance.monaco.editor.create).toHaveBeenCalled();
- });
-
- it('creates dirty diff controller', () => {
- instance.createInstance(el);
-
- expect(instance.dirtyDiffController).not.toBeNull();
- });
- });
-
- describe('createModel', () => {
- it('calls model manager addModel', () => {
- spyOn(instance.modelManager, 'addModel');
-
- instance.createModel('FILE');
-
- expect(instance.modelManager.addModel).toHaveBeenCalledWith('FILE');
- });
- });
-
- describe('attachModel', () => {
- let model;
-
- beforeEach(() => {
- instance.createInstance(document.createElement('div'));
-
- model = instance.createModel(file());
- });
-
- it('sets the current model on the instance', () => {
- instance.attachModel(model);
-
- expect(instance.currentModel).toBe(model);
- });
-
- it('attaches the model to the current instance', () => {
- spyOn(instance.instance, 'setModel');
-
- instance.attachModel(model);
-
- expect(instance.instance.setModel).toHaveBeenCalledWith(model.getModel());
- });
-
- it('attaches the model to the dirty diff controller', () => {
- spyOn(instance.dirtyDiffController, 'attachModel');
-
- instance.attachModel(model);
-
- expect(instance.dirtyDiffController.attachModel).toHaveBeenCalledWith(model);
- });
-
- it('re-decorates with the dirty diff controller', () => {
- spyOn(instance.dirtyDiffController, 'reDecorate');
-
- instance.attachModel(model);
-
- expect(instance.dirtyDiffController.reDecorate).toHaveBeenCalledWith(model);
- });
- });
-
- describe('clearEditor', () => {
- it('resets the editor model', () => {
- instance.createInstance(document.createElement('div'));
-
- spyOn(instance.instance, 'setModel');
-
- instance.clearEditor();
-
- expect(instance.instance.setModel).toHaveBeenCalledWith(null);
- });
- });
-
- describe('dispose', () => {
- it('calls disposble dispose method', () => {
- spyOn(instance.disposable, 'dispose').and.callThrough();
-
- instance.dispose();
-
- expect(instance.disposable.dispose).toHaveBeenCalled();
- });
-
- it('resets instance', () => {
- instance.createInstance(document.createElement('div'));
-
- expect(instance.instance).not.toBeNull();
-
- instance.dispose();
-
- expect(instance.instance).toBeNull();
- });
- });
-});
diff --git a/spec/javascripts/repo/monaco_loader_spec.js b/spec/javascripts/repo/monaco_loader_spec.js
deleted file mode 100644
index b8ac36972aa..00000000000
--- a/spec/javascripts/repo/monaco_loader_spec.js
+++ /dev/null
@@ -1,13 +0,0 @@
-import monacoContext from 'monaco-editor/dev/vs/loader';
-import monacoLoader from '~/ide/monaco_loader';
-
-describe('MonacoLoader', () => {
- it('calls require.config and exports require', () => {
- expect(monacoContext.require.getConfig()).toEqual(jasmine.objectContaining({
- paths: {
- vs: `${__webpack_public_path__}monaco-editor/vs`, // eslint-disable-line camelcase
- },
- }));
- expect(monacoLoader).toBe(monacoContext.require);
- });
-});
diff --git a/spec/javascripts/repo/stores/actions/branch_spec.js b/spec/javascripts/repo/stores/actions/branch_spec.js
deleted file mode 100644
index 00d16fd790d..00000000000
--- a/spec/javascripts/repo/stores/actions/branch_spec.js
+++ /dev/null
@@ -1,44 +0,0 @@
-import store from '~/ide/stores';
-import service from '~/ide/services';
-import { resetStore } from '../../helpers';
-
-describe('Multi-file store branch actions', () => {
- afterEach(() => {
- resetStore(store);
- });
-
- describe('createNewBranch', () => {
- beforeEach(() => {
- spyOn(service, 'createBranch').and.returnValue(Promise.resolve({
- json: () => ({
- name: 'testing',
- }),
- }));
- spyOn(history, 'pushState');
-
- 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.currentBranchId).toBe('testing');
- expect(service.createBranch).toHaveBeenCalledWith('abcproject', {
- branch: 'master',
- ref: 'testing',
- });
-
- done();
- })
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/actions/file_spec.js b/spec/javascripts/repo/stores/actions/file_spec.js
deleted file mode 100644
index e2d8f002e27..00000000000
--- a/spec/javascripts/repo/stores/actions/file_spec.js
+++ /dev/null
@@ -1,431 +0,0 @@
-import Vue from 'vue';
-import store from '~/ide/stores';
-import service from '~/ide/services';
-import { file, resetStore } from '../../helpers';
-
-describe('Multi-file store file actions', () => {
- afterEach(() => {
- resetStore(store);
- });
-
- describe('closeFile', () => {
- let localFile;
- let getLastCommitDataSpy;
- let oldGetLastCommitData;
-
- beforeEach(() => {
- getLastCommitDataSpy = jasmine.createSpy('getLastCommitData');
- oldGetLastCommitData = store._actions.getLastCommitData; // eslint-disable-line
- store._actions.getLastCommitData = [getLastCommitDataSpy]; // eslint-disable-line
-
- localFile = file('testFile');
- localFile.active = true;
- localFile.opened = true;
- localFile.parentTreeUrl = 'parentTreeUrl';
-
- store.state.openFiles.push(localFile);
- });
-
- afterEach(() => {
- store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
- });
-
- it('closes open files', (done) => {
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(localFile.opened).toBeFalsy();
- expect(localFile.active).toBeFalsy();
- expect(store.state.openFiles.length).toBe(0);
-
- done();
- }).catch(done.fail);
- });
-
- it('does not close file if has changed', (done) => {
- localFile.changed = true;
-
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(localFile.opened).toBeTruthy();
- expect(localFile.active).toBeTruthy();
- expect(store.state.openFiles.length).toBe(1);
-
- done();
- }).catch(done.fail);
- });
-
- it('does not close file if temp file', (done) => {
- localFile.tempFile = true;
-
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(localFile.opened).toBeTruthy();
- expect(localFile.active).toBeTruthy();
- expect(store.state.openFiles.length).toBe(1);
-
- done();
- }).catch(done.fail);
- });
-
- it('force closes a changed file', (done) => {
- localFile.changed = true;
-
- store.dispatch('closeFile', { file: localFile, force: true })
- .then(() => {
- expect(localFile.opened).toBeFalsy();
- expect(localFile.active).toBeFalsy();
- expect(store.state.openFiles.length).toBe(0);
-
- done();
- }).catch(done.fail);
- });
-
- it('sets next file as active', (done) => {
- const f = file('otherfile');
- store.state.openFiles.push(f);
-
- expect(f.active).toBeFalsy();
-
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(f.active).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('calls getLastCommitData', (done) => {
- store.dispatch('closeFile', { file: localFile })
- .then(() => {
- expect(getLastCommitDataSpy).toHaveBeenCalled();
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('setFileActive', () => {
- let scrollToTabSpy;
- let oldScrollToTab;
-
- beforeEach(() => {
- scrollToTabSpy = jasmine.createSpy('scrollToTab');
- oldScrollToTab = store._actions.scrollToTab; // eslint-disable-line
- store._actions.scrollToTab = [scrollToTabSpy]; // eslint-disable-line
- });
-
- afterEach(() => {
- store._actions.scrollToTab = oldScrollToTab; // eslint-disable-line
- });
-
- it('calls scrollToTab', (done) => {
- store.dispatch('setFileActive', file('setThisActive'))
- .then(() => {
- expect(scrollToTabSpy).toHaveBeenCalled();
-
- done();
- }).catch(done.fail);
- });
-
- it('sets the file active', (done) => {
- const localFile = file('activeFile');
-
- store.dispatch('setFileActive', localFile)
- .then(() => {
- expect(localFile.active).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('returns early if file is already active', (done) => {
- const localFile = file('earlyActive');
- localFile.active = true;
-
- store.dispatch('setFileActive', localFile)
- .then(() => {
- expect(scrollToTabSpy).not.toHaveBeenCalled();
-
- done();
- }).catch(done.fail);
- });
-
- it('sets current active file to not active', (done) => {
- const localFile = file('currentActive');
- localFile.active = true;
- store.state.openFiles.push(localFile);
-
- store.dispatch('setFileActive', file('newActive'))
- .then(() => {
- expect(localFile.active).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
- it('resets location.hash for line highlighting', (done) => {
- location.hash = 'test';
-
- store.dispatch('setFileActive', file('otherActive'))
- .then(() => {
- expect(location.hash).not.toBe('test');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('getFileData', () => {
- let localFile;
-
- beforeEach(() => {
- spyOn(service, 'getFileData').and.returnValue(Promise.resolve({
- headers: {
- 'page-title': 'testing getFileData',
- },
- json: () => Promise.resolve({
- blame_path: 'blame_path',
- commits_path: 'commits_path',
- permalink: 'permalink',
- raw_path: 'raw_path',
- binary: false,
- html: '123',
- render_error: '',
- }),
- }));
-
- localFile = file('newCreate');
- localFile.url = 'getFileDataURL';
- });
-
- afterEach(() => {
- store.dispatch('closeFile', {
- file: localFile,
- force: true,
- });
- });
-
- it('calls the service', (done) => {
- store.dispatch('getFileData', localFile)
- .then(() => {
- expect(service.getFileData).toHaveBeenCalledWith('getFileDataURL');
-
- done();
- }).catch(done.fail);
- });
-
- it('sets the file data', (done) => {
- store.dispatch('getFileData', localFile)
- .then(Vue.nextTick)
- .then(() => {
- expect(localFile.blamePath).toBe('blame_path');
-
- done();
- }).catch(done.fail);
- });
-
- it('sets document title', (done) => {
- store.dispatch('getFileData', localFile)
- .then(() => {
- expect(document.title).toBe('testing getFileData');
-
- done();
- }).catch(done.fail);
- });
-
- it('sets the file as active', (done) => {
- store.dispatch('getFileData', localFile)
- .then(Vue.nextTick)
- .then(() => {
- expect(localFile.active).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('adds the file to open files', (done) => {
- store.dispatch('getFileData', localFile)
- .then(Vue.nextTick)
- .then(() => {
- expect(store.state.openFiles.length).toBe(1);
- expect(store.state.openFiles[0].name).toBe(localFile.name);
-
- done();
- }).catch(done.fail);
- });
-
- it('toggles the file loading', (done) => {
- store.dispatch('getFileData', localFile)
- .then(() => {
- expect(localFile.loading).toBeTruthy();
-
- return Vue.nextTick();
- })
- .then(() => {
- expect(localFile.loading).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('getRawFileData', () => {
- let tmpFile;
-
- beforeEach(() => {
- spyOn(service, 'getRawFileData').and.returnValue(Promise.resolve('raw'));
-
- tmpFile = file('tmpFile');
- });
-
- it('calls getRawFileData service method', (done) => {
- store.dispatch('getRawFileData', tmpFile)
- .then(() => {
- expect(service.getRawFileData).toHaveBeenCalledWith(tmpFile);
-
- done();
- }).catch(done.fail);
- });
-
- it('updates file raw data', (done) => {
- store.dispatch('getRawFileData', tmpFile)
- .then(() => {
- expect(tmpFile.raw).toBe('raw');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('changeFileContent', () => {
- let tmpFile;
-
- beforeEach(() => {
- tmpFile = file('tmpFile');
- });
-
- it('updates file content', (done) => {
- store.dispatch('changeFileContent', {
- file: tmpFile,
- content: 'content',
- })
- .then(() => {
- expect(tmpFile.content).toBe('content');
-
- done();
- }).catch(done.fail);
- });
- });
-
- 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(() => {
- document.querySelector('.flash-container').remove();
- });
-
- it('creates temp file', (done) => {
- store.dispatch('createTempFile', {
- name: 'test',
- projectId: 'abcproject',
- branchId: 'mybranch',
- parent: projectTree,
- }).then((f) => {
- expect(f.tempFile).toBeTruthy();
- expect(store.state.trees['abcproject/mybranch'].tree.length).toBe(1);
-
- done();
- }).catch(done.fail);
- });
-
- it('adds tmp file to open files', (done) => {
- store.dispatch('createTempFile', {
- 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);
-
- done();
- }).catch(done.fail);
- });
-
- it('sets tmp file as active', (done) => {
- store.dispatch('createTempFile', {
- name: 'test',
- projectId: 'abcproject',
- branchId: 'mybranch',
- parent: projectTree,
- }).then((f) => {
- expect(f.active).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('enters edit mode if file is not base64', (done) => {
- store.dispatch('createTempFile', {
- name: 'test',
- projectId: 'abcproject',
- branchId: 'mybranch',
- parent: projectTree,
- }).then(() => {
- expect(store.state.editMode).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('creates flash message is file already exists', (done) => {
- store.state.trees['abcproject/mybranch'].tree.push(file('test', '1', 'blob'));
-
- store.dispatch('createTempFile', {
- name: 'test',
- projectId: 'abcproject',
- branchId: 'mybranch',
- parent: projectTree,
- }).then(() => {
- expect(document.querySelector('.flash-alert')).not.toBeNull();
-
- done();
- }).catch(done.fail);
- });
-
- it('increases level of file', (done) => {
- store.state.trees['abcproject/mybranch'].level = 1;
-
- store.dispatch('createTempFile', {
- name: 'test',
- projectId: 'abcproject',
- branchId: 'mybranch',
- parent: projectTree,
- }).then((f) => {
- expect(f.level).toBe(2);
-
- done();
- }).catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/actions/tree_spec.js b/spec/javascripts/repo/stores/actions/tree_spec.js
deleted file mode 100644
index 65351dbb7d9..00000000000
--- a/spec/javascripts/repo/stores/actions/tree_spec.js
+++ /dev/null
@@ -1,350 +0,0 @@
-import Vue from 'vue';
-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);
- });
-
- describe('getTreeData', () => {
- beforeEach(() => {
- 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' }],
- }),
- }));
- });
-
- it('calls service getTreeData', (done) => {
- store.dispatch('getTreeData', basicCallParameters)
- .then(() => {
- expect(service.getTreeData).toHaveBeenCalledWith('rootEndpoint');
-
- done();
- }).catch(done.fail);
- });
-
- it('adds data into tree', (done) => {
- store.dispatch('getTreeData', basicCallParameters)
- .then(() => {
- 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', basicCallParameters)
- .then(() => {
- expect(store.state.parentTreeUrl).toBe('parent_tree_url');
-
- done();
- }).catch(done.fail);
- });
-
- it('sets last commit path', (done) => {
- store.dispatch('getTreeData', basicCallParameters)
- .then(() => {
- expect(store.state.trees['abcproject/master'].lastCommitPath).toBe('last_commit_path');
-
- done();
- }).catch(done.fail);
- });
-
- it('sets root if not currently at root', (done) => {
- store.state.isInitialRoot = false;
-
- store.dispatch('getTreeData', basicCallParameters)
- .then(() => {
- expect(store.state.isInitialRoot).toBeTruthy();
- expect(store.state.isRoot).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('sets page title', (done) => {
- store.dispatch('getTreeData', basicCallParameters)
- .then(() => {
- expect(document.title).toBe('test');
-
- 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', basicCallParameters)
- .then(() => {
- expect(getLastCommitDataSpy).toHaveBeenCalledWith(projectTree);
-
- store._actions.getLastCommitData = oldGetLastCommitData; // eslint-disable-line
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('toggleTreeOpen', () => {
- let oldGetTreeData;
- let getTreeDataSpy;
- let tree;
-
- beforeEach(() => {
- getTreeDataSpy = jasmine.createSpy('getTreeData');
-
- oldGetTreeData = store._actions.getTreeData; // eslint-disable-line
- store._actions.getTreeData = [getTreeDataSpy]; // eslint-disable-line
-
- tree = {
- projectId: 'abcproject',
- branchId: 'master',
- opened: false,
- tree: [],
- };
- });
-
- afterEach(() => {
- store._actions.getTreeData = oldGetTreeData; // eslint-disable-line
- });
-
- it('toggles the tree open', (done) => {
- store.dispatch('toggleTreeOpen', {
- endpoint: 'test',
- tree,
- }).then(() => {
- expect(tree.opened).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('calls getTreeData if tree is closed', (done) => {
- store.dispatch('toggleTreeOpen', {
- endpoint: 'test',
- tree,
- }).then(() => {
- expect(getTreeDataSpy).toHaveBeenCalledWith({
- projectId: 'abcproject',
- branch: 'master',
- endpoint: 'test',
- tree,
- });
-
- done();
- }).catch(done.fail);
- });
-
- it('resets entries tree', (done) => {
- Object.assign(tree, {
- opened: true,
- tree: ['a'],
- });
-
- store.dispatch('toggleTreeOpen', {
- endpoint: 'test',
- tree,
- }).then(() => {
- expect(tree.tree.length).toBe(0);
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('createTempTree', () => {
- beforeEach(() => {
- store.state.trees['abcproject/mybranch'] = {
- tree: [],
- };
- projectTree = store.state.trees['abcproject/mybranch'];
- });
-
- 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);
- });
-
- it('creates new folder inside another tree', (done) => {
- const tree = {
- type: 'tree',
- name: 'testing',
- tree: [],
- };
-
- projectTree.tree.push(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);
- });
-
- it('does not create new tree if already exists', (done) => {
- const tree = {
- type: 'tree',
- name: 'testing',
- endpoint: 'test',
- tree: [],
- };
-
- projectTree.tree.push(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].tempFile).toBeUndefined();
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('getLastCommitData', () => {
- beforeEach(() => {
- spyOn(service, 'getTreeLastCommit').and.returnValue(Promise.resolve({
- headers: {
- 'more-logs-url': null,
- },
- json: () => Promise.resolve([{
- type: 'tree',
- file_name: 'testing',
- commit: {
- message: 'commit message',
- authored_date: '123',
- },
- }]),
- }));
-
- 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', projectTree)
- .then(() => {
- expect(service.getTreeLastCommit).toHaveBeenCalledWith('lastcommitpath');
-
- done();
- }).catch(done.fail);
- });
-
- it('updates trees last commit data', (done) => {
- store.dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).toBe('commit message');
-
- done();
- }).catch(done.fail);
- });
-
- it('does not update entry if not found', (done) => {
- projectTree.tree[0].name = 'a';
-
- store.dispatch('getLastCommitData', projectTree)
- .then(Vue.nextTick)
- .then(() => {
- expect(projectTree.tree[0].lastCommit.message).not.toBe('commit message');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('updateDirectoryData', () => {
- it('adds data into tree', (done) => {
- const tree = {
- tree: [],
- };
- const data = {
- trees: [{ name: 'tree' }],
- submodules: [{ name: 'submodule' }],
- blobs: [{ name: 'blob' }],
- };
-
- store.dispatch('updateDirectoryData', {
- data,
- tree,
- }).then(() => {
- expect(tree.tree[0].name).toBe('tree');
- expect(tree.tree[0].type).toBe('tree');
- expect(tree.tree[1].name).toBe('submodule');
- expect(tree.tree[1].type).toBe('submodule');
- expect(tree.tree[2].name).toBe('blob');
- expect(tree.tree[2].type).toBe('blob');
-
- done();
- }).catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/actions_spec.js b/spec/javascripts/repo/stores/actions_spec.js
deleted file mode 100644
index f678967b092..00000000000
--- a/spec/javascripts/repo/stores/actions_spec.js
+++ /dev/null
@@ -1,432 +0,0 @@
-import Vue from 'vue';
-import * as urlUtils from '~/lib/utils/url_utility';
-import store from '~/ide/stores';
-import service from '~/ide/services';
-import { resetStore, file } from '../helpers';
-
-describe('Multi-file store actions', () => {
- afterEach(() => {
- resetStore(store);
- });
-
- describe('redirectToUrl', () => {
- it('calls visitUrl', (done) => {
- spyOn(urlUtils, 'visitUrl');
-
- store.dispatch('redirectToUrl', 'test')
- .then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('test');
-
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('setInitialData', () => {
- it('commits initial data', (done) => {
- store.dispatch('setInitialData', { canCommit: true })
- .then(() => {
- expect(store.state.canCommit).toBeTruthy();
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('closeDiscardPopup', () => {
- it('closes the discard popup', (done) => {
- store.dispatch('closeDiscardPopup', false)
- .then(() => {
- expect(store.state.discardPopupOpen).toBeFalsy();
-
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('discardAllChanges', () => {
- beforeEach(() => {
- store.state.openFiles.push(file('discardAll'));
- store.state.openFiles[0].changed = true;
- });
- });
-
- describe('closeAllFiles', () => {
- beforeEach(() => {
- store.state.openFiles.push(file('closeAll'));
- store.state.openFiles[0].opened = true;
- });
-
- it('closes all open files', (done) => {
- store.dispatch('closeAllFiles')
- .then(() => {
- expect(store.state.openFiles.length).toBe(0);
-
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('toggleEditMode', () => {
- it('toggles edit mode', (done) => {
- store.state.editMode = true;
-
- store.dispatch('toggleEditMode')
- .then(() => {
- expect(store.state.editMode).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
- it('sets preview mode', (done) => {
- store.state.currentBlobView = 'repo-editor';
- store.state.editMode = true;
-
- store.dispatch('toggleEditMode')
- .then(Vue.nextTick)
- .then(() => {
- expect(store.state.currentBlobView).toBe('repo-preview');
-
- done();
- }).catch(done.fail);
- });
-
- it('opens discard popup if there are changed files', (done) => {
- store.state.editMode = true;
- store.state.openFiles.push(file('discardChanges'));
- store.state.openFiles[0].changed = true;
-
- store.dispatch('toggleEditMode')
- .then(() => {
- expect(store.state.discardPopupOpen).toBeTruthy();
-
- done();
- }).catch(done.fail);
- });
-
- it('can force closed if there are changed files', (done) => {
- store.state.editMode = true;
-
- store.state.openFiles.push(file('forceClose'));
- store.state.openFiles[0].changed = true;
-
- store.dispatch('toggleEditMode', true)
- .then(() => {
- expect(store.state.discardPopupOpen).toBeFalsy();
- expect(store.state.editMode).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
-
- it('discards file changes', (done) => {
- const f = file('discard');
- store.state.editMode = true;
- store.state.openFiles.push(f);
- f.changed = true;
-
- store.dispatch('toggleEditMode', true)
- .then(Vue.nextTick)
- .then(() => {
- expect(f.changed).toBeFalsy();
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('toggleBlobView', () => {
- it('sets edit mode view if in edit mode', (done) => {
- store.dispatch('toggleBlobView')
- .then(() => {
- expect(store.state.currentBlobView).toBe('repo-editor');
-
- done();
- })
- .catch(done.fail);
- });
-
- 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');
-
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('checkCommitStatus', () => {
- beforeEach(() => {
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- branches: {
- master: {
- workingReference: '1',
- },
- },
- };
- });
-
- it('calls service', (done) => {
- spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- data: {
- commit: { id: '123' },
- },
- }));
-
- store.dispatch('checkCommitStatus')
- .then(() => {
- expect(service.getBranchData).toHaveBeenCalledWith('abcproject', 'master');
-
- done();
- })
- .catch(done.fail);
- });
-
- it('returns true if current ref does not equal returned ID', (done) => {
- spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- data: {
- commit: { id: '123' },
- },
- }));
-
- store.dispatch('checkCommitStatus')
- .then((val) => {
- expect(val).toBeTruthy();
-
- done();
- })
- .catch(done.fail);
- });
-
- it('returns false if current ref equals returned ID', (done) => {
- spyOn(service, 'getBranchData').and.returnValue(Promise.resolve({
- data: {
- commit: { id: '1' },
- },
- }));
-
- store.dispatch('checkCommitStatus')
- .then((val) => {
- expect(val).toBeFalsy();
-
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('commitChanges', () => {
- let payload;
-
- beforeEach(() => {
- spyOn(window, 'scrollTo');
-
- document.body.innerHTML += '<div class="flash-container"></div>';
-
- store.state.currentProjectId = 'abcproject';
- store.state.currentBranchId = 'master';
- store.state.projects.abcproject = {
- web_url: 'webUrl',
- branches: {
- master: {
- workingReference: '1',
- },
- },
- };
-
- payload = {
- branch: 'master',
- };
- });
-
- afterEach(() => {
- document.querySelector('.flash-container').remove();
- });
-
- describe('success', () => {
- beforeEach(() => {
- spyOn(service, 'commit').and.returnValue(Promise.resolve({
- data: {
- id: '123456',
- short_id: '123',
- message: 'test message',
- committed_date: 'date',
- stats: {
- additions: '1',
- deletions: '2',
- },
- },
- }));
- });
-
- it('calls service', (done) => {
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(service.commit).toHaveBeenCalledWith('abcproject', payload);
-
- done();
- }).catch(done.fail);
- });
-
- it('shows flash notice', (done) => {
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- const alert = document.querySelector('.flash-container');
-
- expect(alert.querySelector('.flash-notice')).not.toBeNull();
- expect(alert.textContent.trim()).toBe(
- 'Your changes have been committed. Commit 123 with 1 additions, 2 deletions.',
- );
-
- done();
- }).catch(done.fail);
- });
-
- it('adds commit data to changed files', (done) => {
- const changedFile = file('changed');
- const f = file('newfile');
- changedFile.changed = true;
-
- store.state.openFiles.push(changedFile, f);
-
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(changedFile.lastCommit.message).toBe('test message');
- expect(f.lastCommit.message).not.toBe('test message');
-
- done();
- }).catch(done.fail);
- });
-
- it('scrolls to top of page', (done) => {
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- expect(window.scrollTo).toHaveBeenCalledWith(0, 0);
-
- done();
- }).catch(done.fail);
- });
-
- it('redirects to new merge request page', (done) => {
- spyOn(urlUtils, 'visitUrl');
-
- store.dispatch('commitChanges', { payload, newMr: true })
- .then(() => {
- expect(urlUtils.visitUrl).toHaveBeenCalledWith('webUrl/merge_requests/new?merge_request%5Bsource_branch%5D=master');
-
- done();
- }).catch(done.fail);
- });
- });
-
- describe('failed', () => {
- beforeEach(() => {
- spyOn(service, 'commit').and.returnValue(Promise.resolve({
- data: {
- message: 'failed message',
- },
- }));
- });
-
- it('shows failed message', (done) => {
- store.dispatch('commitChanges', { payload, newMr: false })
- .then(() => {
- const alert = document.querySelector('.flash-container');
-
- expect(alert.textContent.trim()).toBe(
- 'failed message',
- );
-
- done();
- }).catch(done.fail);
- });
- });
- });
-
- 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(() => {
- const baseTree = projectTree.tree;
- expect(baseTree.length).toBe(1);
- expect(baseTree[0].tempFile).toBeTruthy();
- expect(baseTree[0].type).toBe('tree');
-
- done();
- })
- .catch(done.fail);
- });
-
- 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(() => {
- const baseTree = projectTree.tree;
- expect(baseTree.length).toBe(1);
- expect(baseTree[0].tempFile).toBeTruthy();
- expect(baseTree[0].type).toBe('blob');
-
- done();
- })
- .catch(done.fail);
- });
- });
-
- describe('popHistoryState', () => {
-
- });
-
- describe('scrollToTab', () => {
- it('focuses the current active element', (done) => {
- document.body.innerHTML += '<div id="tabs"><div class="active"><div class="repo-tab"></div></div></div>';
- const el = document.querySelector('.repo-tab');
- spyOn(el, 'focus');
-
- store.dispatch('scrollToTab')
- .then(() => {
- setTimeout(() => {
- expect(el.focus).toHaveBeenCalled();
-
- document.getElementById('tabs').remove();
-
- done();
- });
- })
- .catch(done.fail);
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/getters_spec.js b/spec/javascripts/repo/stores/getters_spec.js
deleted file mode 100644
index d0d5934f29a..00000000000
--- a/spec/javascripts/repo/stores/getters_spec.js
+++ /dev/null
@@ -1,114 +0,0 @@
-import * as getters from '~/ide/stores/getters';
-import state from '~/ide/stores/state';
-import { file } from '../helpers';
-
-describe('Multi-file store getters', () => {
- let localState;
-
- beforeEach(() => {
- localState = state();
- });
-
- describe('changedFiles', () => {
- it('returns a list of changed opened files', () => {
- localState.openFiles.push(file());
- localState.openFiles.push(file('changed'));
- localState.openFiles[1].changed = true;
-
- const changedFiles = getters.changedFiles(localState);
-
- expect(changedFiles.length).toBe(1);
- expect(changedFiles[0].name).toBe('changed');
- });
- });
-
- describe('activeFile', () => {
- it('returns the current active file', () => {
- localState.openFiles.push(file());
- localState.openFiles.push(file('active'));
- localState.openFiles[1].active = true;
-
- expect(getters.activeFile(localState).name).toBe('active');
- });
-
- it('returns undefined if no active files are found', () => {
- localState.openFiles.push(file());
- localState.openFiles.push(file('active'));
-
- expect(getters.activeFile(localState)).toBeNull();
- });
- });
-
- describe('activeFileExtension', () => {
- it('returns the file extension for the current active file', () => {
- localState.openFiles.push(file('active'));
- localState.openFiles[0].active = true;
- localState.openFiles[0].path = 'test.js';
-
- expect(getters.activeFileExtension(localState)).toBe('.js');
-
- localState.openFiles[0].path = 'test.es6.js';
-
- expect(getters.activeFileExtension(localState)).toBe('.js');
- });
- });
-
- describe('canEditFile', () => {
- beforeEach(() => {
- localState.onTopOfBranch = true;
- localState.canCommit = true;
-
- localState.openFiles.push(file());
- localState.openFiles[0].active = true;
- });
-
- it('returns true if user can commit and has open files', () => {
- expect(getters.canEditFile(localState)).toBeTruthy();
- });
-
- it('returns false if user can commit and has no open files', () => {
- localState.openFiles = [];
-
- expect(getters.canEditFile(localState)).toBeFalsy();
- });
-
- it('returns false if user can commit and active file is binary', () => {
- localState.openFiles[0].binary = true;
-
- expect(getters.canEditFile(localState)).toBeFalsy();
- });
-
- it('returns false if user cant commit', () => {
- localState.canCommit = false;
-
- expect(getters.canEditFile(localState)).toBeFalsy();
- });
- });
-
- describe('modifiedFiles', () => {
- it('returns a list of modified files', () => {
- localState.openFiles.push(file());
- localState.openFiles.push(file('changed'));
- localState.openFiles[1].changed = true;
-
- const modifiedFiles = getters.modifiedFiles(localState);
-
- expect(modifiedFiles.length).toBe(1);
- expect(modifiedFiles[0].name).toBe('changed');
- });
- });
-
- describe('addedFiles', () => {
- it('returns a list of added files', () => {
- localState.openFiles.push(file());
- localState.openFiles.push(file('added'));
- localState.openFiles[1].changed = true;
- localState.openFiles[1].tempFile = true;
-
- const modifiedFiles = getters.addedFiles(localState);
-
- expect(modifiedFiles.length).toBe(1);
- expect(modifiedFiles[0].name).toBe('added');
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/mutations/branch_spec.js b/spec/javascripts/repo/stores/mutations/branch_spec.js
deleted file mode 100644
index a7167537ef2..00000000000
--- a/spec/javascripts/repo/stores/mutations/branch_spec.js
+++ /dev/null
@@ -1,18 +0,0 @@
-import mutations from '~/ide/stores/mutations/branch';
-import state from '~/ide/stores/state';
-
-describe('Multi-file store branch mutations', () => {
- let localState;
-
- beforeEach(() => {
- localState = state();
- });
-
- describe('SET_CURRENT_BRANCH', () => {
- it('sets currentBranch', () => {
- mutations.SET_CURRENT_BRANCH(localState, '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
deleted file mode 100644
index 6e204ef0404..00000000000
--- a/spec/javascripts/repo/stores/mutations/file_spec.js
+++ /dev/null
@@ -1,131 +0,0 @@
-import mutations from '~/ide/stores/mutations/file';
-import state from '~/ide/stores/state';
-import { file } from '../../helpers';
-
-describe('Multi-file store file mutations', () => {
- let localState;
- let localFile;
-
- beforeEach(() => {
- localState = state();
- localFile = file();
- });
-
- describe('SET_FILE_ACTIVE', () => {
- it('sets the file active', () => {
- mutations.SET_FILE_ACTIVE(localState, {
- file: localFile,
- active: true,
- });
-
- expect(localFile.active).toBeTruthy();
- });
- });
-
- describe('TOGGLE_FILE_OPEN', () => {
- beforeEach(() => {
- mutations.TOGGLE_FILE_OPEN(localState, localFile);
- });
-
- it('adds into opened files', () => {
- expect(localFile.opened).toBeTruthy();
- expect(localState.openFiles.length).toBe(1);
- });
-
- it('removes from opened files', () => {
- mutations.TOGGLE_FILE_OPEN(localState, localFile);
-
- expect(localFile.opened).toBeFalsy();
- expect(localState.openFiles.length).toBe(0);
- });
- });
-
- describe('SET_FILE_DATA', () => {
- it('sets extra file data', () => {
- mutations.SET_FILE_DATA(localState, {
- data: {
- blame_path: 'blame',
- commits_path: 'commits',
- permalink: 'permalink',
- raw_path: 'raw',
- binary: true,
- html: 'html',
- render_error: 'render_error',
- },
- file: localFile,
- });
-
- expect(localFile.blamePath).toBe('blame');
- expect(localFile.commitsPath).toBe('commits');
- expect(localFile.permalink).toBe('permalink');
- expect(localFile.rawPath).toBe('raw');
- expect(localFile.binary).toBeTruthy();
- expect(localFile.html).toBe('html');
- expect(localFile.renderError).toBe('render_error');
- });
- });
-
- describe('SET_FILE_RAW_DATA', () => {
- it('sets raw data', () => {
- mutations.SET_FILE_RAW_DATA(localState, {
- file: localFile,
- raw: 'testing',
- });
-
- expect(localFile.raw).toBe('testing');
- });
- });
-
- describe('UPDATE_FILE_CONTENT', () => {
- beforeEach(() => {
- localFile.raw = 'test';
- });
-
- it('sets content', () => {
- mutations.UPDATE_FILE_CONTENT(localState, {
- file: localFile,
- content: 'test',
- });
-
- expect(localFile.content).toBe('test');
- });
-
- it('sets changed if content does not match raw', () => {
- mutations.UPDATE_FILE_CONTENT(localState, {
- file: localFile,
- content: 'testing',
- });
-
- expect(localFile.content).toBe('testing');
- expect(localFile.changed).toBeTruthy();
- });
- });
-
- describe('DISCARD_FILE_CHANGES', () => {
- beforeEach(() => {
- localFile.content = 'test';
- localFile.changed = true;
- });
-
- it('resets content and changed', () => {
- mutations.DISCARD_FILE_CHANGES(localState, localFile);
-
- expect(localFile.content).toBe('');
- expect(localFile.changed).toBeFalsy();
- });
- });
-
- describe('CREATE_TMP_FILE', () => {
- it('adds file into parent tree', () => {
- const f = file('tmpFile');
-
- mutations.CREATE_TMP_FILE(localState, {
- file: f,
- parent: localFile,
- });
-
- expect(localFile.tree.length).toBe(1);
- expect(localFile.tree[0].name).toBe(f.name);
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/mutations/tree_spec.js b/spec/javascripts/repo/stores/mutations/tree_spec.js
deleted file mode 100644
index e6ca8ea139e..00000000000
--- a/spec/javascripts/repo/stores/mutations/tree_spec.js
+++ /dev/null
@@ -1,71 +0,0 @@
-import mutations from '~/ide/stores/mutations/tree';
-import state from '~/ide/stores/state';
-import { file } from '../../helpers';
-
-describe('Multi-file store tree mutations', () => {
- let localState;
- let localTree;
-
- beforeEach(() => {
- localState = state();
- localTree = file();
- });
-
- describe('TOGGLE_TREE_OPEN', () => {
- it('toggles tree open', () => {
- mutations.TOGGLE_TREE_OPEN(localState, localTree);
-
- expect(localTree.opened).toBeTruthy();
-
- mutations.TOGGLE_TREE_OPEN(localState, localTree);
-
- expect(localTree.opened).toBeFalsy();
- });
- });
-
- describe('SET_DIRECTORY_DATA', () => {
- const data = [{
- name: 'tree',
- },
- {
- name: 'submodule',
- },
- {
- name: 'blob',
- }];
-
- it('adds directory data', () => {
- mutations.SET_DIRECTORY_DATA(localState, {
- data,
- tree: localState,
- });
-
- expect(localState.tree.length).toBe(3);
- expect(localState.tree[0].name).toBe('tree');
- expect(localState.tree[1].name).toBe('submodule');
- expect(localState.tree[2].name).toBe('blob');
- });
- });
-
- describe('SET_PARENT_TREE_URL', () => {
- it('sets the parent tree url', () => {
- mutations.SET_PARENT_TREE_URL(localState, 'test');
-
- expect(localState.parentTreeUrl).toBe('test');
- });
- });
-
- describe('CREATE_TMP_TREE', () => {
- it('adds tree into parent tree', () => {
- const tmpEntry = file('tmpTree');
-
- mutations.CREATE_TMP_TREE(localState, {
- tmpEntry,
- parent: localTree,
- });
-
- expect(localTree.tree.length).toBe(1);
- expect(localTree.tree[0].name).toBe(tmpEntry.name);
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/mutations_spec.js b/spec/javascripts/repo/stores/mutations_spec.js
deleted file mode 100644
index 5fd8ad94972..00000000000
--- a/spec/javascripts/repo/stores/mutations_spec.js
+++ /dev/null
@@ -1,125 +0,0 @@
-import mutations from '~/ide/stores/mutations';
-import state from '~/ide/stores/state';
-import { file } from '../helpers';
-
-describe('Multi-file store mutations', () => {
- let localState;
- let entry;
-
- beforeEach(() => {
- localState = state();
- entry = file();
- });
-
- describe('SET_INITIAL_DATA', () => {
- it('sets all initial data', () => {
- mutations.SET_INITIAL_DATA(localState, {
- test: 'test',
- });
-
- expect(localState.test).toBe('test');
- });
- });
-
- describe('SET_PREVIEW_MODE', () => {
- it('sets currentBlobView to repo-preview', () => {
- mutations.SET_PREVIEW_MODE(localState);
-
- expect(localState.currentBlobView).toBe('repo-preview');
-
- localState.currentBlobView = 'testing';
-
- mutations.SET_PREVIEW_MODE(localState);
-
- expect(localState.currentBlobView).toBe('repo-preview');
- });
- });
-
- describe('SET_EDIT_MODE', () => {
- it('sets currentBlobView to repo-editor', () => {
- mutations.SET_EDIT_MODE(localState);
-
- expect(localState.currentBlobView).toBe('repo-editor');
-
- localState.currentBlobView = 'testing';
-
- mutations.SET_EDIT_MODE(localState);
-
- expect(localState.currentBlobView).toBe('repo-editor');
- });
- });
-
- describe('TOGGLE_LOADING', () => {
- it('toggles loading of entry', () => {
- mutations.TOGGLE_LOADING(localState, entry);
-
- expect(entry.loading).toBeTruthy();
-
- mutations.TOGGLE_LOADING(localState, entry);
-
- expect(entry.loading).toBeFalsy();
- });
- });
-
- describe('TOGGLE_EDIT_MODE', () => {
- it('toggles editMode', () => {
- mutations.TOGGLE_EDIT_MODE(localState);
-
- expect(localState.editMode).toBeFalsy();
-
- mutations.TOGGLE_EDIT_MODE(localState);
-
- expect(localState.editMode).toBeTruthy();
- });
- });
-
- describe('TOGGLE_DISCARD_POPUP', () => {
- it('sets discardPopupOpen', () => {
- mutations.TOGGLE_DISCARD_POPUP(localState, true);
-
- expect(localState.discardPopupOpen).toBeTruthy();
-
- mutations.TOGGLE_DISCARD_POPUP(localState, false);
-
- expect(localState.discardPopupOpen).toBeFalsy();
- });
- });
-
- describe('SET_ROOT', () => {
- it('sets isRoot & initialRoot', () => {
- mutations.SET_ROOT(localState, true);
-
- expect(localState.isRoot).toBeTruthy();
- expect(localState.isInitialRoot).toBeTruthy();
-
- mutations.SET_ROOT(localState, false);
-
- expect(localState.isRoot).toBeFalsy();
- expect(localState.isInitialRoot).toBeFalsy();
- });
- });
-
- 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.rightPanelCollapsed).toBeFalsy();
- });
- });
-});
diff --git a/spec/javascripts/repo/stores/utils_spec.js b/spec/javascripts/repo/stores/utils_spec.js
deleted file mode 100644
index 89745a2029e..00000000000
--- a/spec/javascripts/repo/stores/utils_spec.js
+++ /dev/null
@@ -1,119 +0,0 @@
-import * as utils from '~/ide/stores/utils';
-import state from '~/ide/stores/state';
-import { file } from '../helpers';
-
-describe('Multi-file store utils', () => {
- describe('setPageTitle', () => {
- it('sets the document page title', () => {
- utils.setPageTitle('test');
-
- expect(document.title).toBe('test');
- });
- });
-
- describe('treeList', () => {
- let localState;
-
- 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(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);
- });
- });
-
- describe('createTemp', () => {
- it('creates temp tree', () => {
- const tmp = utils.createTemp({
- name: 'test',
- path: 'test',
- type: 'tree',
- level: 0,
- changed: false,
- content: '',
- base64: '',
- });
-
- expect(tmp.tempFile).toBeTruthy();
- expect(tmp.icon).toBe('fa-folder');
- });
-
- it('creates temp file', () => {
- const tmp = utils.createTemp({
- name: 'test',
- path: 'test',
- type: 'blob',
- level: 0,
- changed: false,
- content: '',
- base64: '',
- });
-
- expect(tmp.tempFile).toBeTruthy();
- expect(tmp.icon).toBe('fa-file-text-o');
- });
- });
-
- describe('findIndexOfFile', () => {
- let localState;
-
- beforeEach(() => {
- localState = [{
- path: '1',
- }, {
- path: '2',
- }];
- });
-
- it('finds in the index of an entry by path', () => {
- const index = utils.findIndexOfFile(localState, {
- path: '2',
- });
-
- expect(index).toBe(1);
- });
- });
-
- describe('findEntry', () => {
- let localState;
-
- beforeEach(() => {
- localState = {
- tree: [{
- type: 'tree',
- name: 'test',
- }, {
- type: 'blob',
- name: 'file',
- }],
- };
- });
-
- it('returns an entry found by name', () => {
- 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(localState.tree, 'blob', 'test');
-
- expect(foundEntry).toBeUndefined();
- });
- });
-});
diff --git a/spec/javascripts/sidebar/assignees_spec.js b/spec/javascripts/sidebar/assignees_spec.js
index c9453a21189..4e4343812bd 100644
--- a/spec/javascripts/sidebar/assignees_spec.js
+++ b/spec/javascripts/sidebar/assignees_spec.js
@@ -1,5 +1,5 @@
import Vue from 'vue';
-import Assignee from '~/sidebar/components/assignees/assignees';
+import Assignee from '~/sidebar/components/assignees/assignees.vue';
import UsersMock from './mock_data';
import UsersMockHelper from '../helpers/user_mock_data_helper';
diff --git a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
index b0ea8ae0206..deeea669de8 100644
--- a/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
+++ b/spec/javascripts/sidebar/lock/edit_form_buttons_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import editFormButtons from '~/sidebar/components/lock/edit_form_buttons.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('EditFormButtons', () => {
let vm1;
diff --git a/spec/javascripts/sidebar/participants_spec.js b/spec/javascripts/sidebar/participants_spec.js
index 30cc549c7c0..2a3b60c399c 100644
--- a/spec/javascripts/sidebar/participants_spec.js
+++ b/spec/javascripts/sidebar/participants_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import participants from '~/sidebar/components/participants/participants.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const PARTICIPANT = {
id: 1,
diff --git a/spec/javascripts/sidebar/sidebar_assignees_spec.js b/spec/javascripts/sidebar/sidebar_assignees_spec.js
index 6bb6d639f24..2fbb7268e0b 100644
--- a/spec/javascripts/sidebar/sidebar_assignees_spec.js
+++ b/spec/javascripts/sidebar/sidebar_assignees_spec.js
@@ -4,8 +4,8 @@ import SidebarAssignees from '~/sidebar/components/assignees/sidebar_assignees';
import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Mock from './mock_data';
-import mountComponent from '../helpers/vue_mount_component_helper';
describe('sidebar assignees', () => {
let vm;
diff --git a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
index a6113cb0bae..56a2543660b 100644
--- a/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
+++ b/spec/javascripts/sidebar/sidebar_subscriptions_spec.js
@@ -4,7 +4,7 @@ import SidebarMediator from '~/sidebar/sidebar_mediator';
import SidebarService from '~/sidebar/services/sidebar_service';
import SidebarStore from '~/sidebar/stores/sidebar_store';
import eventHub from '~/sidebar/event_hub';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import Mock from './mock_data';
describe('Sidebar Subscriptions', function () {
diff --git a/spec/javascripts/sidebar/subscriptions_spec.js b/spec/javascripts/sidebar/subscriptions_spec.js
index 79db05f04ed..aee8f0acbb9 100644
--- a/spec/javascripts/sidebar/subscriptions_spec.js
+++ b/spec/javascripts/sidebar/subscriptions_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import subscriptions from '~/sidebar/components/subscriptions/subscriptions.vue';
-import mountComponent from '../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Subscriptions', function () {
let vm;
diff --git a/spec/javascripts/test_bundle.js b/spec/javascripts/test_bundle.js
index 323b8a9572d..1bcfdfe72b6 100644
--- a/spec/javascripts/test_bundle.js
+++ b/spec/javascripts/test_bundle.js
@@ -37,6 +37,7 @@ window.$ = window.jQuery = $;
window.gl = window.gl || {};
window.gl.TEST_HOST = 'http://test.host';
window.gon = window.gon || {};
+window.gon.test_env = true;
let hasUnhandledPromiseRejections = false;
@@ -113,7 +114,9 @@ if (process.env.BABEL_ENV === 'coverage') {
// exempt these files from the coverage report
const troubleMakers = [
'./blob_edit/blob_bundle.js',
- './boards/boards_bundle.js',
+ './boards/components/modal/empty_state.js',
+ './boards/components/modal/footer.js',
+ './boards/components/modal/header.js',
'./cycle_analytics/cycle_analytics_bundle.js',
'./cycle_analytics/components/stage_plan_component.js',
'./cycle_analytics/components/stage_staging_component.js',
@@ -124,7 +127,6 @@ if (process.env.BABEL_ENV === 'coverage') {
'./diff_notes/components/resolve_count.js',
'./dispatcher.js',
'./environments/environments_bundle.js',
- './filtered_search/filtered_search_bundle.js',
'./graphs/graphs_bundle.js',
'./issuable/time_tracking/time_tracking_bundle.js',
'./main.js',
diff --git a/spec/javascripts/u2f/authenticate_spec.js b/spec/javascripts/u2f/authenticate_spec.js
index 29b15f3a782..4d15bcc4956 100644
--- a/spec/javascripts/u2f/authenticate_spec.js
+++ b/spec/javascripts/u2f/authenticate_spec.js
@@ -5,7 +5,7 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FAuthenticate', () => {
preloadFixtures('u2f/authenticate.html.raw');
- beforeEach(() => {
+ beforeEach((done) => {
loadFixtures('u2f/authenticate.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-authenticate-u2f');
@@ -22,7 +22,7 @@ describe('U2FAuthenticate', () => {
// bypass automatic form submission within renderAuthenticated
spyOn(this.component, 'renderAuthenticated').and.returnValue(true);
- return this.component.start();
+ this.component.start().then(done).catch(done.fail);
});
it('allows authenticating via a U2F device', () => {
@@ -34,7 +34,7 @@ describe('U2FAuthenticate', () => {
expect(this.component.renderAuthenticated).toHaveBeenCalledWith('{"deviceData":"this is data from the device"}');
});
- return describe('errors', () => {
+ describe('errors', () => {
it('displays an error message', () => {
const setupButton = this.container.find('#js-login-u2f-device');
setupButton.trigger('click');
diff --git a/spec/javascripts/u2f/register_spec.js b/spec/javascripts/u2f/register_spec.js
index b0051f11362..dbe89c2923c 100644
--- a/spec/javascripts/u2f/register_spec.js
+++ b/spec/javascripts/u2f/register_spec.js
@@ -5,12 +5,12 @@ import MockU2FDevice from './mock_u2f_device';
describe('U2FRegister', () => {
preloadFixtures('u2f/register.html.raw');
- beforeEach(() => {
+ beforeEach((done) => {
loadFixtures('u2f/register.html.raw');
this.u2fDevice = new MockU2FDevice();
this.container = $('#js-register-u2f');
this.component = new U2FRegister(this.container, $('#js-register-u2f-templates'), {}, 'token');
- return this.component.start();
+ this.component.start().then(done).catch(done.fail);
});
it('allows registering a U2F device', () => {
diff --git a/spec/javascripts/u2f/util_spec.js b/spec/javascripts/u2f/util_spec.js
new file mode 100644
index 00000000000..4187183236f
--- /dev/null
+++ b/spec/javascripts/u2f/util_spec.js
@@ -0,0 +1,45 @@
+import { canInjectU2fApi } from '~/u2f/util';
+
+describe('U2F Utils', () => {
+ describe('canInjectU2fApi', () => {
+ it('returns false for Chrome < 41', () => {
+ const userAgent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/40.0.2214.28 Safari/537.36';
+ expect(canInjectU2fApi(userAgent)).toBe(false);
+ });
+
+ it('returns true for Chrome >= 41', () => {
+ const userAgent = 'Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/63.0.3239.132 Safari/537.36';
+ expect(canInjectU2fApi(userAgent)).toBe(true);
+ });
+
+ it('returns false for Opera < 40', () => {
+ const userAgent = 'Mozilla/5.0 (Windows NT 6.1; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/45.0.2454.85 Safari/537.36 OPR/32.0.1948.25';
+ expect(canInjectU2fApi(userAgent)).toBe(false);
+ });
+
+ it('returns true for Opera >= 40', () => {
+ const userAgent = 'Mozilla/5.0 (Windows NT 10.0; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/56.0.2924.87 Safari/537.36 OPR/43.0.2442.991';
+ expect(canInjectU2fApi(userAgent)).toBe(true);
+ });
+
+ it('returns false for Safari', () => {
+ const userAgent = 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_5) AppleWebKit/603.2.4 (KHTML, like Gecko) Version/10.1.1 Safari/603.2.4';
+ expect(canInjectU2fApi(userAgent)).toBe(false);
+ });
+
+ it('returns false for Chrome on Android', () => {
+ const userAgent = 'Mozilla/5.0 (Linux; Android 7.0; VS988 Build/NRD90U) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/61.0.3145.0 Mobile Safari/537.36';
+ expect(canInjectU2fApi(userAgent)).toBe(false);
+ });
+
+ it('returns false for Chrome on iOS', () => {
+ const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/602.1.50 (KHTML, like Gecko) CriOS/56.0.2924.75 Mobile/14E5239e Safari/602.1';
+ expect(canInjectU2fApi(userAgent)).toBe(false);
+ });
+
+ it('returns false for Safari on iOS', () => {
+ const userAgent = 'Mozilla/5.0 (iPhone; CPU iPhone OS 11_0 like Mac OS X) AppleWebKit/604.1.38 (KHTML, like Gecko) Version/11.0 Mobile/15A356 Safari/604.1';
+ expect(canInjectU2fApi(userAgent)).toBe(false);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
index f14d5f6f76c..db27aa144d6 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import authorComponent from '~/vue_merge_request_widget/components/mr_widget_author.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAuthor', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
index 8c55622b15e..6784b498c29 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_author_time_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import authorTimeComponent from '~/vue_merge_request_widget/components/mr_widget_author_time.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAuthorTime', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
index 13e5595bbfc..235c33fac0d 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_header_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import headerComponent from '~/vue_merge_request_widget/components/mr_widget_header.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetHeader', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
index cc43639f576..367c499daaf 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_merge_help_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import mergeHelpComponent from '~/vue_merge_request_widget/components/mr_widget_merge_help.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMergeHelp', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index d7af956c9c1..431cb7f3913 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import pipelineComponent from '~/vue_merge_request_widget/components/mr_widget_pipeline.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from '../mock_data';
describe('MRWidgetPipeline', () => {
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
index 66ecaa316c8..b453d180a40 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_rebase_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
import component from '~/vue_merge_request_widget/components/states/mr_widget_rebase.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Merge request widget rebase component', () => {
let Component;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
index 637bf483deb..5de6ac4079d 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_related_links_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import relatedLinksComponent from '~/vue_merge_request_widget/components/mr_widget_related_links.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetRelatedLinks', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
index c39fcda0071..0b25500caf4 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_status_icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import mrStatusIcon from '~/vue_merge_request_widget/components/mr_widget_status_icon.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MR widget status icon component', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
index f98ebdb38e6..e818f87b4c8 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_archived_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import archivedComponent from '~/vue_merge_request_widget/components/states/mr_widget_archived.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetArchived', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
index 95c94e95e3a..d069dc3fcc6 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_auto_merge_failed_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import autoMergeFailedComponent from '~/vue_merge_request_widget/components/states/mr_widget_auto_merge_failed.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetAutoMergeFailed', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
index 658cadddb81..658612aad3c 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_checking_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import checkingComponent from '~/vue_merge_request_widget/components/states/mr_widget_checking.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetChecking', () => {
let Component;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
index 51a34739ee9..0e3c134d3ac 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_closed_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import closedComponent from '~/vue_merge_request_widget/components/states/mr_widget_closed.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetClosed', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
index a7d69fdcdb9..5323523abc0 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_conflicts_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import conflictsComponent from '~/vue_merge_request_widget/components/states/mr_widget_conflicts.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetConflicts', () => {
let Component;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
index a57b9811e08..dd1d62cd4ed 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_failed_to_merge_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import failedToMergeComponent from '~/vue_merge_request_widget/components/states/mr_widget_failed_to_merge.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetFailedToMerge', () => {
let Component;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
index df56c4e2c5c..dd907ad9015 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merge_when_pipeline_succeeds_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import mwpsComponent from '~/vue_merge_request_widget/components/states/mr_widget_merge_when_pipeline_succeeds.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMergeWhenPipelineSucceeds', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
index 43a989393ba..c2c92d8ac56 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merged_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import mergedComponent from '~/vue_merge_request_widget/components/states/mr_widget_merged.vue';
import eventHub from '~/vue_merge_request_widget/event_hub';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMerged', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
index 0b2ed2d4086..d2d219e4bdb 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_merging_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import mergingComponent from '~/vue_merge_request_widget/components/states/mr_widget_merging.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMerging', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
index 3d7f4abd420..34f76b39b28 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_missing_branch_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import missingBranchComponent from '~/vue_merge_request_widget/components/states/mr_widget_missing_branch.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetMissingBranch', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
index c89e863d904..9f8b96c118b 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_not_allowed_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import notAllowedComponent from '~/vue_merge_request_widget/components/states/mr_widget_not_allowed.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetNotAllowed', () => {
let vm;
diff --git a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
index edab26286bc..baacbc03fb1 100644
--- a/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/states/mr_widget_pipeline_blocked_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import pipelineBlockedComponent from '~/vue_merge_request_widget/components/states/mr_widget_pipeline_blocked.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('MRWidgetPipelineBlocked', () => {
let vm;
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 45035effe81..18ba34b55a5 100644
--- a/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
+++ b/spec/javascripts/vue_mr_widget/mr_widget_options_spec.js
@@ -3,8 +3,8 @@ 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 mountComponent from 'spec/helpers/vue_mount_component_helper';
import mockData from './mock_data';
-import mountComponent from '../helpers/vue_mount_component_helper';
const returnPromise = data => new Promise((resolve) => {
resolve({
diff --git a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
index 8762ce9903b..668742ebaee 100644
--- a/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
+++ b/spec/javascripts/vue_shared/components/ci_badge_link_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import ciBadge from '~/vue_shared/components/ci_badge_link.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('CI Badge Link Component', () => {
let CIBadge;
diff --git a/spec/javascripts/vue_shared/components/clipboard_button_spec.js b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
index 08e4e1f8337..d0fc10d69ea 100644
--- a/spec/javascripts/vue_shared/components/clipboard_button_spec.js
+++ b/spec/javascripts/vue_shared/components/clipboard_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import clipboardButton from '~/vue_shared/components/clipboard_button.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('clipboard button', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/expand_button_spec.js b/spec/javascripts/vue_shared/components/expand_button_spec.js
index a33ab689dd1..f19589d3b75 100644
--- a/spec/javascripts/vue_shared/components/expand_button_spec.js
+++ b/spec/javascripts/vue_shared/components/expand_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import expandButton from '~/vue_shared/components/expand_button.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('expand button', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/file_icon_spec.js b/spec/javascripts/vue_shared/components/file_icon_spec.js
index d99b17bdc79..f7581251bf0 100644
--- a/spec/javascripts/vue_shared/components/file_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/file_icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import fileIcon from '~/vue_shared/components/file_icon.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('File Icon component', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/gl_modal_spec.js b/spec/javascripts/vue_shared/components/gl_modal_spec.js
index d6148cb785b..2805d9a7003 100644
--- a/spec/javascripts/vue_shared/components/gl_modal_spec.js
+++ b/spec/javascripts/vue_shared/components/gl_modal_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import GlModal from '~/vue_shared/components/gl_modal.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const modalComponent = Vue.extend(GlModal);
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index b378a0bd896..65499a2d730 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import headerCi from '~/vue_shared/components/header_ci_component.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Header CI Component', () => {
let HeaderCi;
diff --git a/spec/javascripts/vue_shared/components/icon_spec.js b/spec/javascripts/vue_shared/components/icon_spec.js
index a22b6bd3a67..68d57ebc8f0 100644
--- a/spec/javascripts/vue_shared/components/icon_spec.js
+++ b/spec/javascripts/vue_shared/components/icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import Icon from '~/vue_shared/components/icon.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Sprite Icon Component', function () {
describe('Initialization', function () {
diff --git a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
index 24484796bf1..e6ed77dbb52 100644
--- a/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
+++ b/spec/javascripts/vue_shared/components/issue/issue_warning_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import issueWarning from '~/vue_shared/components/issue/issue_warning.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const IssueWarning = Vue.extend(issueWarning);
diff --git a/spec/javascripts/vue_shared/components/loading_button_spec.js b/spec/javascripts/vue_shared/components/loading_button_spec.js
index 49bf8ee6f7c..51c19cd4080 100644
--- a/spec/javascripts/vue_shared/components/loading_button_spec.js
+++ b/spec/javascripts/vue_shared/components/loading_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import loadingButton from '~/vue_shared/components/loading_button.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const LABEL = 'Hello';
diff --git a/spec/javascripts/vue_shared/components/modal_spec.js b/spec/javascripts/vue_shared/components/modal_spec.js
index a5f9c75be4e..8412df74f98 100644
--- a/spec/javascripts/vue_shared/components/modal_spec.js
+++ b/spec/javascripts/vue_shared/components/modal_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import modal from '~/vue_shared/components/modal.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const modalComponent = Vue.extend(modal);
diff --git a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
index 78e7d747b92..09fda95d7d3 100644
--- a/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
+++ b/spec/javascripts/vue_shared/components/navigation_tabs_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import navigationTabs from '~/vue_shared/components/navigation_tabs.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('navigation tabs component', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
index 7b8e6c330c2..262571efcb8 100644
--- a/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
+++ b/spec/javascripts/vue_shared/components/notes/placeholder_system_note_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import placeholderSystemNote from '~/vue_shared/components/notes/placeholder_system_note.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('placeholder system note component', () => {
let PlaceholderSystemNote;
diff --git a/spec/javascripts/vue_shared/components/panel_resizer_spec.js b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
index 70ce3dffaba..8efcb54659d 100644
--- a/spec/javascripts/vue_shared/components/panel_resizer_spec.js
+++ b/spec/javascripts/vue_shared/components/panel_resizer_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import panelResizer from '~/vue_shared/components/panel_resizer.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Panel Resizer component', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/pikaday_spec.js b/spec/javascripts/vue_shared/components/pikaday_spec.js
index 47af9534737..b349e2a2a81 100644
--- a/spec/javascripts/vue_shared/components/pikaday_spec.js
+++ b/spec/javascripts/vue_shared/components/pikaday_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import datePicker from '~/vue_shared/components/pikaday.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('datePicker', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
index cce53193870..8c296af6652 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_calendar_icon_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import collapsedCalendarIcon from '~/vue_shared/components/sidebar/collapsed_calendar_icon.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('collapsedCalendarIcon', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
index 2de108da2ac..9d60f9c758f 100644
--- a/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/collapsed_grouped_date_picker_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import collapsedGroupedDatePicker from '~/vue_shared/components/sidebar/collapsed_grouped_date_picker.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('collapsedGroupedDatePicker', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
index 926e11b4d30..8840a5a9dbf 100644
--- a/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/date_picker_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import sidebarDatePicker from '~/vue_shared/components/sidebar/date_picker.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('sidebarDatePicker', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
new file mode 100644
index 00000000000..67056793a20
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/base_spec.js
@@ -0,0 +1,81 @@
+import Vue from 'vue';
+
+import LabelsSelect from '~/labels_select';
+import baseComponent from '~/vue_shared/components/sidebar/labels_select/base.vue';
+
+import { mockConfig, mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (config = mockConfig) => {
+ const Component = Vue.extend(baseComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('BaseComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('hiddenInputName', () => {
+ it('returns correct string when showCreate prop is `true`', () => {
+ expect(vm.hiddenInputName).toBe('issue[label_names][]');
+ });
+
+ it('returns correct string when showCreate prop is `false`', () => {
+ const mockConfigNonEditable = Object.assign({}, mockConfig, { showCreate: false });
+ const vmNonEditable = createComponent(mockConfigNonEditable);
+ expect(vmNonEditable.hiddenInputName).toBe('label_id[]');
+ vmNonEditable.$destroy();
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('handleClick', () => {
+ it('emits onLabelClick event with label and list of labels as params', () => {
+ spyOn(vm, '$emit');
+ vm.handleClick(mockLabels[0]);
+ expect(vm.$emit).toHaveBeenCalledWith('onLabelClick', mockLabels[0]);
+ });
+ });
+ });
+
+ describe('mounted', () => {
+ it('creates LabelsSelect object and assigns it to `labelsDropdon` as prop', () => {
+ expect(vm.labelsDropdown instanceof LabelsSelect).toBe(true);
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `block labels`', () => {
+ expect(vm.$el.classList.contains('block')).toBe(true);
+ expect(vm.$el.classList.contains('labels')).toBe(true);
+ });
+
+ it('renders `.selectbox` element', () => {
+ expect(vm.$el.querySelector('.selectbox')).not.toBeNull();
+ expect(vm.$el.querySelector('.selectbox').getAttribute('style')).toBe('display: none;');
+ });
+
+ it('renders `.dropdown` element', () => {
+ expect(vm.$el.querySelector('.dropdown')).not.toBeNull();
+ });
+
+ it('renders `.dropdown-menu` element', () => {
+ const dropdownMenuEl = vm.$el.querySelector('.dropdown-menu');
+ expect(dropdownMenuEl).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-page-one')).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-content')).not.toBeNull();
+ expect(dropdownMenuEl.querySelector('.dropdown-loading')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
new file mode 100644
index 00000000000..ec63ac306d0
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_button_spec.js
@@ -0,0 +1,82 @@
+import Vue from 'vue';
+
+import dropdownButtonComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_button.vue';
+
+import { mockConfig, mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const componentConfig = Object.assign({}, mockConfig, {
+ fieldName: 'label_id[]',
+ labels: mockLabels,
+ showExtraOptions: false,
+});
+
+const createComponent = (config = componentConfig) => {
+ const Component = Vue.extend(dropdownButtonComponent);
+
+ return mountComponent(Component, config);
+};
+
+describe('DropdownButtonComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('dropdownToggleText', () => {
+ it('returns text as `Label` when `labels` prop is empty array', () => {
+ const mockEmptyLabels = Object.assign({}, componentConfig, { labels: [] });
+ const vmEmptyLabels = createComponent(mockEmptyLabels);
+ expect(vmEmptyLabels.dropdownToggleText).toBe('Label');
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns first label name with remaining label count when `labels` prop has more than one item', () => {
+ const mockMoreLabels = Object.assign({}, componentConfig, {
+ labels: mockLabels.concat(mockLabels),
+ });
+ const vmMoreLabels = createComponent(mockMoreLabels);
+ expect(vmMoreLabels.dropdownToggleText).toBe('Foo Label +1 more');
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns first label name when `labels` prop has only one item present', () => {
+ expect(vm.dropdownToggleText).toBe('Foo Label');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element of type `button`', () => {
+ expect(vm.$el.nodeName).toBe('BUTTON');
+ });
+
+ it('renders component container element with required data attributes', () => {
+ expect(vm.$el.dataset.abilityName).toBe(vm.abilityName);
+ expect(vm.$el.dataset.fieldName).toBe(vm.fieldName);
+ expect(vm.$el.dataset.issueUpdate).toBe(vm.updatePath);
+ expect(vm.$el.dataset.labels).toBe(vm.labelsPath);
+ expect(vm.$el.dataset.namespacePath).toBe(vm.namespace);
+ expect(vm.$el.dataset.showAny).not.toBeDefined();
+ });
+
+ it('renders dropdown toggle text element', () => {
+ const dropdownToggleTextEl = vm.$el.querySelector('.dropdown-toggle-text');
+ expect(dropdownToggleTextEl).not.toBeNull();
+ expect(dropdownToggleTextEl.innerText.trim()).toBe('Foo Label');
+ });
+
+ it('renders dropdown button icon', () => {
+ const dropdownIconEl = vm.$el.querySelector('i.fa');
+ expect(dropdownIconEl).not.toBeNull();
+ expect(dropdownIconEl.classList.contains('fa-chevron-down')).toBe(true);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
new file mode 100644
index 00000000000..f07aefb2f87
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_create_label_spec.js
@@ -0,0 +1,84 @@
+import Vue from 'vue';
+
+import dropdownCreateLabelComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_create_label.vue';
+
+import { mockSuggestedColors } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownCreateLabelComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownCreateLabelComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ gon.suggested_label_colors = mockSuggestedColors;
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('created', () => {
+ it('initializes `suggestedColors` prop on component from `gon.suggested_color_labels` object', () => {
+ expect(vm.suggestedColors.length).toBe(mockSuggestedColors.length);
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `dropdown-page-two dropdown-new-label`', () => {
+ expect(vm.$el.classList.contains('dropdown-page-two', 'dropdown-new-label')).toBe(true);
+ });
+
+ it('renders `Go back` button on component header', () => {
+ const backButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-back');
+ expect(backButtonEl).not.toBe(null);
+ expect(backButtonEl.querySelector('.fa-arrow-left')).not.toBe(null);
+ });
+
+ it('renders component header element', () => {
+ const headerEl = vm.$el.querySelector('.dropdown-title');
+ expect(headerEl.innerText.trim()).toContain('Create new label');
+ });
+
+ it('renders `Close` button on component header', () => {
+ const closeButtonEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close');
+ expect(closeButtonEl).not.toBe(null);
+ expect(closeButtonEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBe(null);
+ });
+
+ it('renders `Name new label` input element', () => {
+ expect(vm.$el.querySelector('.dropdown-labels-error.js-label-error')).not.toBe(null);
+ expect(vm.$el.querySelector('input#new_label_name.default-dropdown-input')).not.toBe(null);
+ });
+
+ it('renders suggested colors list elements', () => {
+ const colorsListContainerEl = vm.$el.querySelector('.suggest-colors.suggest-colors-dropdown');
+ expect(colorsListContainerEl).not.toBe(null);
+ expect(colorsListContainerEl.querySelectorAll('a').length).toBe(mockSuggestedColors.length);
+
+ const colorItemEl = colorsListContainerEl.querySelectorAll('a')[0];
+ expect(colorItemEl.dataset.color).toBe(vm.suggestedColors[0]);
+ expect(colorItemEl.getAttribute('style')).toBe('background-color: rgb(0, 51, 204);');
+ });
+
+ it('renders color input element', () => {
+ expect(vm.$el.querySelector('.dropdown-label-color-input')).not.toBe(null);
+ expect(vm.$el.querySelector('.dropdown-label-color-preview.js-dropdown-label-color-preview')).not.toBe(null);
+ expect(vm.$el.querySelector('input#new_label_color.default-dropdown-input')).not.toBe(null);
+ });
+
+ it('renders component action buttons', () => {
+ const createBtnEl = vm.$el.querySelector('button.js-new-label-btn');
+ const cancelBtnEl = vm.$el.querySelector('button.js-cancel-label-btn');
+ expect(createBtnEl).not.toBe(null);
+ expect(createBtnEl.innerText.trim()).toBe('Create');
+ expect(cancelBtnEl.innerText.trim()).toBe('Cancel');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
new file mode 100644
index 00000000000..809e0327b1c
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_footer_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+
+import dropdownFooterComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_footer.vue';
+
+import { mockConfig } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (labelsWebUrl = mockConfig.labelsWebUrl) => {
+ const Component = Vue.extend(dropdownFooterComponent);
+
+ return mountComponent(Component, {
+ labelsWebUrl,
+ });
+};
+
+describe('DropdownFooterComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders `Create new label` link element', () => {
+ const createLabelEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-toggle-page');
+ expect(createLabelEl).not.toBeNull();
+ expect(createLabelEl.innerText.trim()).toBe('Create new label');
+ });
+
+ it('renders `Manage labels` link element', () => {
+ const manageLabelsEl = vm.$el.querySelector('.dropdown-footer-list .dropdown-external-link');
+ expect(manageLabelsEl).not.toBeNull();
+ expect(manageLabelsEl.getAttribute('href')).toBe(vm.labelsWebUrl);
+ expect(manageLabelsEl.innerText.trim()).toBe('Manage labels');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
new file mode 100644
index 00000000000..325fa47c957
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_header_spec.js
@@ -0,0 +1,36 @@
+import Vue from 'vue';
+
+import dropdownHeaderComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_header.vue';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownHeaderComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownHeaderComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders header text element', () => {
+ const headerEl = vm.$el.querySelector('.dropdown-title span');
+ expect(headerEl.innerText.trim()).toBe('Assign labels');
+ });
+
+ it('renders `Close` button element', () => {
+ const closeBtnEl = vm.$el.querySelector('.dropdown-title button.dropdown-title-button.dropdown-menu-close');
+ expect(closeBtnEl).not.toBeNull();
+ expect(closeBtnEl.querySelector('.fa-times.dropdown-menu-close-icon')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
new file mode 100644
index 00000000000..703b87498c7
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_hidden_input_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+
+import dropdownHiddenInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_hidden_input.vue';
+
+import { mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (name = 'label_id[]', label = mockLabels[0]) => {
+ const Component = Vue.extend(dropdownHiddenInputComponent);
+
+ return mountComponent(Component, {
+ name,
+ label,
+ });
+};
+
+describe('DropdownHiddenInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element of type `hidden`', () => {
+ expect(vm.$el.nodeName).toBe('INPUT');
+ expect(vm.$el.getAttribute('type')).toBe('hidden');
+ expect(vm.$el.getAttribute('name')).toBe(vm.name);
+ expect(vm.$el.getAttribute('value')).toBe(`${vm.label.id}`);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
new file mode 100644
index 00000000000..69e11d966c2
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_search_input_spec.js
@@ -0,0 +1,39 @@
+import Vue from 'vue';
+
+import dropdownSearchInputComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_search_input.vue';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = () => {
+ const Component = Vue.extend(dropdownSearchInputComponent);
+
+ return mountComponent(Component);
+};
+
+describe('DropdownSearchInputComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders input element with type `search`', () => {
+ const inputEl = vm.$el.querySelector('input.dropdown-input-field');
+ expect(inputEl).not.toBeNull();
+ expect(inputEl.getAttribute('type')).toBe('search');
+ });
+
+ it('renders search icon element', () => {
+ expect(vm.$el.querySelector('.fa-search.dropdown-input-search')).not.toBeNull();
+ });
+
+ it('renders clear search icon element', () => {
+ expect(vm.$el.querySelector('.fa-times.dropdown-input-clear.js-dropdown-input-clear')).not.toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
new file mode 100644
index 00000000000..c3580933072
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_title_spec.js
@@ -0,0 +1,42 @@
+import Vue from 'vue';
+
+import dropdownTitleComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_title.vue';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (canEdit = true) => {
+ const Component = Vue.extend(dropdownTitleComponent);
+
+ return mountComponent(Component, {
+ canEdit,
+ });
+};
+
+describe('DropdownTitleComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('template', () => {
+ it('renders title text', () => {
+ expect(vm.$el.classList.contains('title', 'hide-collapsed')).toBe(true);
+ expect(vm.$el.innerText.trim()).toContain('Labels');
+ });
+
+ it('renders spinner icon element', () => {
+ expect(vm.$el.querySelector('.fa-spinner.fa-spin.block-loading')).not.toBeNull();
+ });
+
+ it('renders `Edit` button element', () => {
+ const editBtnEl = vm.$el.querySelector('button.edit-link.js-sidebar-dropdown-toggle');
+ expect(editBtnEl).not.toBeNull();
+ expect(editBtnEl.innerText.trim()).toBe('Edit');
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
new file mode 100644
index 00000000000..93b42795bea
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed_spec.js
@@ -0,0 +1,74 @@
+import Vue from 'vue';
+
+import dropdownValueCollapsedComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value_collapsed.vue';
+
+import { mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (labels = mockLabels) => {
+ const Component = Vue.extend(dropdownValueCollapsedComponent);
+
+ return mountComponent(Component, {
+ labels,
+ });
+};
+
+describe('DropdownValueCollapsedComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('labelsList', () => {
+ it('returns empty text when `labels` prop is empty array', () => {
+ const vmEmptyLabels = createComponent([]);
+ expect(vmEmptyLabels.labelsList).toBe('');
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns labels names separated by coma when `labels` prop has more than one item', () => {
+ const vmMoreLabels = createComponent(mockLabels.concat(mockLabels));
+ expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label');
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns labels names separated by coma with remaining labels count and `and more` phrase when `labels` prop has more than five items', () => {
+ const mockMoreLabels = Object.assign([], mockLabels);
+ for (let i = 0; i < 6; i += 1) {
+ mockMoreLabels.unshift(mockLabels[0]);
+ }
+
+ const vmMoreLabels = createComponent(mockMoreLabels);
+ expect(vmMoreLabels.labelsList).toBe('Foo Label, Foo Label, Foo Label, Foo Label, Foo Label, and 2 more');
+ vmMoreLabels.$destroy();
+ });
+
+ it('returns first label name when `labels` prop has only one item present', () => {
+ expect(vm.labelsList).toBe('Foo Label');
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with tooltip`', () => {
+ expect(vm.$el.dataset.placement).toBe('left');
+ expect(vm.$el.dataset.container).toBe('body');
+ expect(vm.$el.dataset.originalTitle).toBe(vm.labelsList);
+ });
+
+ it('renders tags icon element', () => {
+ expect(vm.$el.querySelector('.fa-tags')).not.toBeNull();
+ });
+
+ it('renders labels count', () => {
+ expect(vm.$el.querySelector('span').innerText.trim()).toBe(`${vm.labels.length}`);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
new file mode 100644
index 00000000000..66e0957b431
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/dropdown_value_spec.js
@@ -0,0 +1,94 @@
+import Vue from 'vue';
+
+import dropdownValueComponent from '~/vue_shared/components/sidebar/labels_select/dropdown_value.vue';
+
+import { mockConfig, mockLabels } from './mock_data';
+
+import mountComponent from '../../../../helpers/vue_mount_component_helper';
+
+const createComponent = (
+ labels = mockLabels,
+ labelFilterBasePath = mockConfig.labelFilterBasePath,
+) => {
+ const Component = Vue.extend(dropdownValueComponent);
+
+ return mountComponent(Component, {
+ labels,
+ labelFilterBasePath,
+ });
+};
+
+describe('DropdownValueComponent', () => {
+ let vm;
+
+ beforeEach(() => {
+ vm = createComponent();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('computed', () => {
+ describe('isEmpty', () => {
+ it('returns true if `labels` prop is empty', () => {
+ const vmEmptyLabels = createComponent([]);
+ expect(vmEmptyLabels.isEmpty).toBe(true);
+ vmEmptyLabels.$destroy();
+ });
+
+ it('returns false if `labels` prop is empty', () => {
+ expect(vm.isEmpty).toBe(false);
+ });
+ });
+ });
+
+ describe('methods', () => {
+ describe('labelFilterUrl', () => {
+ it('returns URL string starting with labelFilterBasePath and encoded label.title', () => {
+ expect(vm.labelFilterUrl({
+ title: 'Foo bar',
+ })).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20bar');
+ });
+ });
+
+ describe('labelStyle', () => {
+ it('returns object with `color` & `backgroundColor` properties from label.textColor & label.color', () => {
+ const label = {
+ textColor: '#FFFFFF',
+ color: '#BADA55',
+ };
+ const styleObj = vm.labelStyle(label);
+
+ expect(styleObj.color).toBe(label.textColor);
+ expect(styleObj.backgroundColor).toBe(label.color);
+ });
+ });
+ });
+
+ describe('template', () => {
+ it('renders component container element with classes `hide-collapsed value issuable-show-labels`', () => {
+ expect(vm.$el.classList.contains('hide-collapsed', 'value', 'issuable-show-labels')).toBe(true);
+ });
+
+ it('render slot content inside component when `labels` prop is empty', () => {
+ const vmEmptyLabels = createComponent([]);
+ expect(vmEmptyLabels.$el.querySelector('.text-secondary').innerText.trim()).toBe(mockConfig.emptyValueText);
+ vmEmptyLabels.$destroy();
+ });
+
+ it('renders label element with filter URL', () => {
+ expect(vm.$el.querySelector('a').getAttribute('href')).toBe('/gitlab-org/my-project/issues?label_name[]=Foo%20Label');
+ });
+
+ it('renders label element with tooltip and styles based on label details', () => {
+ const labelEl = vm.$el.querySelector('a span.label.color-label');
+ expect(labelEl).not.toBeNull();
+ expect(labelEl.dataset.placement).toBe('bottom');
+ expect(labelEl.dataset.container).toBe('body');
+ expect(labelEl.dataset.originalTitle).toBe(mockLabels[0].description);
+ expect(labelEl.getAttribute('style')).toBe('background-color: rgb(186, 218, 85);');
+ expect(labelEl.innerText.trim()).toBe(mockLabels[0].title);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
new file mode 100644
index 00000000000..e9008c29b22
--- /dev/null
+++ b/spec/javascripts/vue_shared/components/sidebar/labels_select/mock_data.js
@@ -0,0 +1,49 @@
+export const mockLabels = [
+ {
+ id: 26,
+ title: 'Foo Label',
+ description: 'Foobar',
+ color: '#BADA55',
+ text_color: '#FFFFFF',
+ },
+];
+
+export const mockSuggestedColors = [
+ '#0033CC',
+ '#428BCA',
+ '#44AD8E',
+ '#A8D695',
+ '#5CB85C',
+ '#69D100',
+ '#004E00',
+ '#34495E',
+ '#7F8C8D',
+ '#A295D6',
+ '#5843AD',
+ '#8E44AD',
+ '#FFECDB',
+ '#AD4363',
+ '#D10069',
+ '#CC0033',
+ '#FF0000',
+ '#D9534F',
+ '#D1D100',
+ '#F0AD4E',
+ '#AD8D43',
+];
+
+export const mockConfig = {
+ showCreate: true,
+ abilityName: 'issue',
+ context: {
+ labels: mockLabels,
+ },
+ namespace: 'gitlab-org',
+ updatePath: '/gitlab-org/my-project/issue/1',
+ labelsPath: '/gitlab-org/my-project/labels.json',
+ labelsWebUrl: '/gitlab-org/my-project/labels',
+ labelFilterBasePath: '/gitlab-org/my-project/issues',
+ canEdit: true,
+ suggestedColors: mockSuggestedColors,
+ emptyValueText: 'None',
+};
diff --git a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
index 752a9e89d50..c911a129173 100644
--- a/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
+++ b/spec/javascripts/vue_shared/components/sidebar/toggle_sidebar_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import toggleSidebar from '~/vue_shared/components/sidebar/toggle_sidebar.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('toggleSidebar', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
index a5db0b2c59e..bbd50863069 100644
--- a/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
+++ b/spec/javascripts/vue_shared/components/skeleton_loading_container_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import skeletonLoadingContainer from '~/vue_shared/components/skeleton_loading_container.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Skeleton loading container', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
index 6940b04573e..de3bf667fb3 100644
--- a/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
+++ b/spec/javascripts/vue_shared/components/stacked_progress_bar_spec.js
@@ -2,7 +2,7 @@ import Vue from 'vue';
import stackedProgressBarComponent from '~/vue_shared/components/stacked_progress_bar.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const createComponent = (config) => {
const Component = Vue.extend(stackedProgressBarComponent);
diff --git a/spec/javascripts/vue_shared/components/toggle_button_spec.js b/spec/javascripts/vue_shared/components/toggle_button_spec.js
index 859995d33fa..71952cc39e0 100644
--- a/spec/javascripts/vue_shared/components/toggle_button_spec.js
+++ b/spec/javascripts/vue_shared/components/toggle_button_spec.js
@@ -1,6 +1,6 @@
import Vue from 'vue';
import toggleButton from '~/vue_shared/components/toggle_button.vue';
-import mountComponent from '../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
describe('Toggle Button', () => {
let vm;
diff --git a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
index aa93134f2dd..446f025c127 100644
--- a/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
+++ b/spec/javascripts/vue_shared/components/user_avatar/user_avatar_image_spec.js
@@ -1,7 +1,7 @@
import Vue from 'vue';
import { placeholderImage } from '~/lazy_loader';
import userAvatarImage from '~/vue_shared/components/user_avatar/user_avatar_image.vue';
-import mountComponent from '../../../helpers/vue_mount_component_helper';
+import mountComponent from 'spec/helpers/vue_mount_component_helper';
const DEFAULT_PROPS = {
size: 99,
diff --git a/spec/lib/backup/repository_spec.rb b/spec/lib/backup/repository_spec.rb
index f7b1a61f4f8..a9b5ed1112a 100644
--- a/spec/lib/backup/repository_spec.rb
+++ b/spec/lib/backup/repository_spec.rb
@@ -28,6 +28,23 @@ describe Backup::Repository do
end
describe '#restore' do
+ subject { described_class.new }
+
+ let(:timestamp) { Time.utc(2017, 3, 22) }
+ let(:temp_dirs) do
+ Gitlab.config.repositories.storages.map do |name, storage|
+ File.join(storage['path'], '..', 'repositories.old.' + timestamp.to_i.to_s)
+ end
+ end
+
+ around do |example|
+ Timecop.freeze(timestamp) { example.run }
+ end
+
+ after do
+ temp_dirs.each { |path| FileUtils.rm_rf(path) }
+ end
+
describe 'command failure' do
before do
allow(Gitlab::Popen).to receive(:popen).and_return(['error', 1])
@@ -35,7 +52,7 @@ describe Backup::Repository do
context 'hashed storage' do
it 'shows the appropriate error' do
- described_class.new.restore
+ subject.restore
expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} (#{project.disk_path}) - error")
end
@@ -45,7 +62,7 @@ describe Backup::Repository do
let!(:project) { create(:project, :legacy_storage) }
it 'shows the appropriate error' do
- described_class.new.restore
+ subject.restore
expect(progress).to have_received(:puts).with("Ignoring error on #{project.full_path} - error")
end
diff --git a/spec/lib/banzai/filter/autolink_filter_spec.rb b/spec/lib/banzai/filter/autolink_filter_spec.rb
index b7c2ff03125..b502daea418 100644
--- a/spec/lib/banzai/filter/autolink_filter_spec.rb
+++ b/spec/lib/banzai/filter/autolink_filter_spec.rb
@@ -4,6 +4,7 @@ describe Banzai::Filter::AutolinkFilter do
include FilterSpecHelper
let(:link) { 'http://about.gitlab.com/' }
+ let(:quotes) { ['"', "'"] }
it 'does nothing when :autolink is false' do
exp = act = link
@@ -15,17 +16,7 @@ describe Banzai::Filter::AutolinkFilter do
expect(filter(act).to_html).to eq exp
end
- context 'when the input contains no links' do
- it 'does not parse_html back the rinku returned value' do
- act = HTML::Pipeline.parse('<p>This text contains no links to autolink</p>')
-
- expect_any_instance_of(described_class).not_to receive(:parse_html)
-
- filter(act).to_html
- end
- end
-
- context 'Rinku schemes' do
+ context 'Various schemes' do
it 'autolinks http' do
doc = filter("See #{link}")
expect(doc.at_css('a').text).to eq link
@@ -56,32 +47,26 @@ describe Banzai::Filter::AutolinkFilter do
expect(doc.at_css('a')['href']).to eq link
end
- it 'accepts link_attr options' do
- doc = filter("See #{link}", link_attr: { class: 'custom' })
+ it 'autolinks multiple URLs' do
+ link1 = 'http://localhost:3000/'
+ link2 = 'http://google.com/'
- expect(doc.at_css('a')['class']).to eq 'custom'
- end
+ doc = filter("See #{link1} and #{link2}")
- described_class::IGNORE_PARENTS.each do |elem|
- it "ignores valid links contained inside '#{elem}' element" do
- exp = act = "<#{elem}>See #{link}</#{elem}>"
- expect(filter(act).to_html).to eq exp
- end
- end
+ found_links = doc.css('a')
- context 'when the input contains link' do
- it 'does parse_html back the rinku returned value' do
- act = HTML::Pipeline.parse("<p>See #{link}</p>")
+ expect(found_links.size).to eq(2)
+ expect(found_links[0].text).to eq(link1)
+ expect(found_links[0]['href']).to eq(link1)
+ expect(found_links[1].text).to eq(link2)
+ expect(found_links[1]['href']).to eq(link2)
+ end
- expect_any_instance_of(described_class).to receive(:parse_html).at_least(:once).and_call_original
+ it 'accepts link_attr options' do
+ doc = filter("See #{link}", link_attr: { class: 'custom' })
- filter(act).to_html
- end
+ expect(doc.at_css('a')['class']).to eq 'custom'
end
- end
-
- context 'other schemes' do
- let(:link) { 'foo://bar.baz/' }
it 'autolinks smb' do
link = 'smb:///Volumes/shared/foo.pdf'
@@ -91,6 +76,21 @@ describe Banzai::Filter::AutolinkFilter do
expect(doc.at_css('a')['href']).to eq link
end
+ it 'autolinks multiple occurences of smb' do
+ link1 = 'smb:///Volumes/shared/foo.pdf'
+ link2 = 'smb:///Volumes/shared/bar.pdf'
+
+ doc = filter("See #{link1} and #{link2}")
+
+ found_links = doc.css('a')
+
+ expect(found_links.size).to eq(2)
+ expect(found_links[0].text).to eq(link1)
+ expect(found_links[0]['href']).to eq(link1)
+ expect(found_links[1].text).to eq(link2)
+ expect(found_links[1]['href']).to eq(link2)
+ end
+
it 'autolinks irc' do
link = 'irc://irc.freenode.net/git'
doc = filter("See #{link}")
@@ -132,6 +132,45 @@ describe Banzai::Filter::AutolinkFilter do
expect(doc.at_css('a').text).to eq link
end
+ it 'includes trailing punctuation when part of a balanced pair' do
+ described_class::PUNCTUATION_PAIRS.each do |close, open|
+ next if open.in?(quotes)
+
+ balanced_link = "#{link}#{open}abc#{close}"
+ balanced_actual = filter("See #{balanced_link}...")
+ unbalanced_link = "#{link}#{close}"
+ unbalanced_actual = filter("See #{unbalanced_link}...")
+
+ expect(balanced_actual.at_css('a').text).to eq(balanced_link)
+ expect(unescape(balanced_actual.to_html)).to eq(Rinku.auto_link("See #{balanced_link}..."))
+ expect(unbalanced_actual.at_css('a').text).to eq(link)
+ expect(unescape(unbalanced_actual.to_html)).to eq(Rinku.auto_link("See #{unbalanced_link}..."))
+ end
+ end
+
+ it 'removes trailing quotes' do
+ quotes.each do |quote|
+ balanced_link = "#{link}#{quote}abc#{quote}"
+ balanced_actual = filter("See #{balanced_link}...")
+ unbalanced_link = "#{link}#{quote}"
+ unbalanced_actual = filter("See #{unbalanced_link}...")
+
+ expect(balanced_actual.at_css('a').text).to eq(balanced_link[0...-1])
+ expect(unescape(balanced_actual.to_html)).to eq(Rinku.auto_link("See #{balanced_link}..."))
+ expect(unbalanced_actual.at_css('a').text).to eq(link)
+ expect(unescape(unbalanced_actual.to_html)).to eq(Rinku.auto_link("See #{unbalanced_link}..."))
+ end
+ end
+
+ it 'removes one closing punctuation mark when the punctuation in the link is unbalanced' do
+ complicated_link = "(#{link}(a'b[c'd]))'"
+ expected_complicated_link = %Q{(<a href="#{link}(a'b[c'd]))">#{link}(a'b[c'd]))</a>'}
+ actual = unescape(filter(complicated_link).to_html)
+
+ expect(actual).to eq(Rinku.auto_link(complicated_link))
+ expect(actual).to eq(expected_complicated_link)
+ end
+
it 'does not include trailing HTML entities' do
doc = filter("See &lt;&lt;&lt;#{link}&gt;&gt;&gt;")
@@ -151,4 +190,29 @@ describe Banzai::Filter::AutolinkFilter do
end
end
end
+
+ context 'when the link is inside a tag' do
+ %w[http rdar].each do |protocol|
+ it "renders text after the link correctly for #{protocol}" do
+ doc = filter(ERB::Util.html_escape_once("<#{protocol}://link><another>"))
+
+ expect(doc.children.last.text).to include('<another>')
+ end
+ end
+ end
+
+ # Rinku does not escape these characters in HTML attributes, but content_tag
+ # does. We don't care about that difference for these specs, though.
+ def unescape(html)
+ %w([ ] { }).each do |cgi_escape|
+ html.sub!(CGI.escape(cgi_escape), cgi_escape)
+ end
+
+ quotes.each do |html_escape|
+ html.sub!(CGI.escape_html(html_escape), html_escape)
+ html.sub!(CGI.escape(html_escape), CGI.escape_html(html_escape))
+ end
+
+ html
+ end
end
diff --git a/spec/lib/banzai/filter/label_reference_filter_spec.rb b/spec/lib/banzai/filter/label_reference_filter_spec.rb
index 862b1fe3fd3..0c524a1551f 100644
--- a/spec/lib/banzai/filter/label_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/label_reference_filter_spec.rb
@@ -381,11 +381,11 @@ describe Banzai::Filter::LabelReferenceFilter do
end
it 'has valid link text' do
- expect(result.css('a').first.text).to eq "#{label.name} in #{project2.name_with_namespace}"
+ expect(result.css('a').first.text).to eq "#{label.name} in #{project2.full_name}"
end
it 'has valid text' do
- expect(result.text).to eq "See #{label.name} in #{project2.name_with_namespace}"
+ expect(result.text).to eq "See #{label.name} in #{project2.full_name}"
end
it 'ignores invalid IDs on the referenced label' do
@@ -481,12 +481,12 @@ describe Banzai::Filter::LabelReferenceFilter do
it 'has valid link text' do
expect(result.css('a').first.text)
- .to eq "#{group_label.name} in #{another_project.name_with_namespace}"
+ .to eq "#{group_label.name} in #{another_project.full_name}"
end
it 'has valid text' do
expect(result.text)
- .to eq "See #{group_label.name} in #{another_project.name_with_namespace}"
+ .to eq "See #{group_label.name} in #{another_project.full_name}"
end
it 'ignores invalid IDs on the referenced label' do
diff --git a/spec/lib/banzai/redactor_spec.rb b/spec/lib/banzai/redactor_spec.rb
index 1fa89137972..441f3725985 100644
--- a/spec/lib/banzai/redactor_spec.rb
+++ b/spec/lib/banzai/redactor_spec.rb
@@ -40,6 +40,16 @@ describe Banzai::Redactor do
expect(doc.to_html).to eq(original_content)
end
end
+
+ it 'returns <a> tag with original href if it is originally a link reference' do
+ href = 'http://localhost:3000'
+ doc = Nokogiri::HTML
+ .fragment("<a class='gfm' data-reference-type='issue' data-original=#{href} data-link-reference='true'>#{href}</a>")
+
+ redactor.redact([doc])
+
+ expect(doc.to_html).to eq('<a href="http://localhost:3000">http://localhost:3000</a>')
+ end
end
context 'when project is in pending delete' do
diff --git a/spec/lib/gitlab/ldap/access_spec.rb b/spec/lib/gitlab/auth/ldap/access_spec.rb
index 6a47350be81..9b3916bf9e3 100644
--- a/spec/lib/gitlab/ldap/access_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/access_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Access do
+describe Gitlab::Auth::LDAP::Access do
let(:access) { described_class.new user }
let(:user) { create(:omniauth_user) }
@@ -19,7 +19,7 @@ describe Gitlab::LDAP::Access do
context 'when the user cannot be found' do
before do
- allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
end
it { is_expected.to be_falsey }
@@ -33,12 +33,12 @@ describe Gitlab::LDAP::Access do
context 'when the user is found' do
before do
- allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(:ldap_user)
end
context 'and the user is disabled via active directory' do
before do
- allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(true)
end
it { is_expected.to be_falsey }
@@ -52,7 +52,7 @@ describe Gitlab::LDAP::Access do
context 'and has no disabled flag in active diretory' do
before do
- allow(Gitlab::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:disabled_via_active_directory?).and_return(false)
end
it { is_expected.to be_truthy }
@@ -87,15 +87,15 @@ describe Gitlab::LDAP::Access do
context 'without ActiveDirectory enabled' do
before do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
- allow_any_instance_of(Gitlab::LDAP::Config).to receive(:active_directory).and_return(false)
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive(:active_directory).and_return(false)
end
it { is_expected.to be_truthy }
context 'when user cannot be found' do
before do
- allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(nil)
end
it { is_expected.to be_falsey }
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/auth/ldap/adapter_spec.rb
index 6132abd9b35..10c60d792bd 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/adapter_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Adapter do
+describe Gitlab::Auth::LDAP::Adapter do
include LdapHelpers
let(:ldap) { double(:ldap) }
@@ -139,6 +139,6 @@ describe Gitlab::LDAP::Adapter do
end
def ldap_attributes
- Gitlab::LDAP::Person.ldap_attributes(Gitlab::LDAP::Config.new('ldapmain'))
+ Gitlab::Auth::LDAP::Person.ldap_attributes(Gitlab::Auth::LDAP::Config.new('ldapmain'))
end
end
diff --git a/spec/lib/gitlab/ldap/auth_hash_spec.rb b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
index 9c30ddd7fe2..05541972f87 100644
--- a/spec/lib/gitlab/ldap/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/auth_hash_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::AuthHash do
+describe Gitlab::Auth::LDAP::AuthHash do
include LdapHelpers
let(:auth_hash) do
@@ -56,7 +56,7 @@ describe Gitlab::LDAP::AuthHash do
end
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive(:attributes).and_return(attributes)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive(:attributes).and_return(attributes)
end
it "has the correct username" do
diff --git a/spec/lib/gitlab/ldap/authentication_spec.rb b/spec/lib/gitlab/auth/ldap/authentication_spec.rb
index 9d57a46c12b..111572d043b 100644
--- a/spec/lib/gitlab/ldap/authentication_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/authentication_spec.rb
@@ -1,14 +1,14 @@
require 'spec_helper'
-describe Gitlab::LDAP::Authentication do
+describe Gitlab::Auth::LDAP::Authentication do
let(:dn) { 'uid=John Smith, ou=People, dc=example, dc=com' }
- let(:user) { create(:omniauth_user, extern_uid: Gitlab::LDAP::Person.normalize_dn(dn)) }
+ let(:user) { create(:omniauth_user, extern_uid: Gitlab::Auth::LDAP::Person.normalize_dn(dn)) }
let(:login) { 'john' }
let(:password) { 'password' }
describe 'login' do
before do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end
it "finds the user if authentication is successful" do
@@ -43,7 +43,7 @@ describe Gitlab::LDAP::Authentication do
end
it "fails if ldap is disabled" do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(false)
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(false)
expect(described_class.login(login, password)).to be_falsey
end
diff --git a/spec/lib/gitlab/ldap/config_spec.rb b/spec/lib/gitlab/auth/ldap/config_spec.rb
index e10837578a8..82587e2ba55 100644
--- a/spec/lib/gitlab/ldap/config_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/config_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Config do
+describe Gitlab::Auth::LDAP::Config do
include LdapHelpers
let(:config) { described_class.new('ldapmain') }
diff --git a/spec/lib/gitlab/ldap/dn_spec.rb b/spec/lib/gitlab/auth/ldap/dn_spec.rb
index 8e21ecdf9ab..f2983a02602 100644
--- a/spec/lib/gitlab/ldap/dn_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/dn_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::DN do
+describe Gitlab::Auth::LDAP::DN do
using RSpec::Parameterized::TableSyntax
describe '#normalize_value' do
@@ -13,7 +13,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'John Smith,' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
@@ -21,7 +21,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '#aa aa' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
end
end
@@ -29,7 +29,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '#aaXaaa' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
end
end
@@ -37,7 +37,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '#aaaYaa' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
end
end
@@ -45,7 +45,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '"Sebasti\\cX\\a1n"' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
end
end
@@ -53,7 +53,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '"James' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
@@ -61,7 +61,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'J\ames' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
end
end
@@ -69,7 +69,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'foo\\' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
end
@@ -86,7 +86,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'uid=john smith+telephonenumber=+1 555-555-5555,ou=people,dc=example,dc=com' }
it 'raises UnsupportedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError)
end
end
@@ -95,7 +95,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'uid = John Smith + telephoneNumber = + 1 555-555-5555 , ou = People,dc=example,dc=com' }
it 'raises UnsupportedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError)
end
end
@@ -103,7 +103,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'uid = John Smith + telephoneNumber = +1 555-555-5555 , ou = People,dc=example,dc=com' }
it 'raises UnsupportedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::UnsupportedError)
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::UnsupportedError)
end
end
end
@@ -115,7 +115,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'uid=John Smith,' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
@@ -123,7 +123,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '0.9.2342.19200300.100.1.25=#aa aa' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the end of an attribute value, but got \"a\"")
end
end
@@ -131,7 +131,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '0.9.2342.19200300.100.1.25=#aaXaaa' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the first character of a hex pair, but got \"X\"")
end
end
@@ -139,7 +139,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '0.9.2342.19200300.100.1.25=#aaaYaa' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair, but got \"Y\"")
end
end
@@ -147,7 +147,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'uid="Sebasti\\cX\\a1n"' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, "Expected the second character of a hex pair inside a double quoted value, but got \"X\"")
end
end
@@ -155,7 +155,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'John' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
@@ -163,7 +163,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'cn="James' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
@@ -171,7 +171,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'cn=J\ames' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Invalid escaped hex code "\am"')
end
end
@@ -179,7 +179,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'cn=\\' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'DN string ended unexpectedly')
end
end
@@ -187,7 +187,7 @@ describe Gitlab::LDAP::DN do
let(:given) { '1.2.d=Value' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN OID attribute type name character "d"')
end
end
@@ -195,7 +195,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'd1.2=Value' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "."')
end
end
@@ -203,7 +203,7 @@ describe Gitlab::LDAP::DN do
let(:given) { ' -uid=John Smith' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized first character of an RDN attribute type name "-"')
end
end
@@ -211,7 +211,7 @@ describe Gitlab::LDAP::DN do
let(:given) { 'uid\\=john' }
it 'raises MalformedError' do
- expect { subject }.to raise_error(Gitlab::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"')
+ expect { subject }.to raise_error(Gitlab::Auth::LDAP::DN::MalformedError, 'Unrecognized RDN attribute type name character "\\"')
end
end
end
diff --git a/spec/lib/gitlab/ldap/person_spec.rb b/spec/lib/gitlab/auth/ldap/person_spec.rb
index 05e1e394bb1..1527fe60fb9 100644
--- a/spec/lib/gitlab/ldap/person_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/person_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::Person do
+describe Gitlab::Auth::LDAP::Person do
include LdapHelpers
let(:entry) { ldap_user_entry('john.doe') }
@@ -59,7 +59,7 @@ describe Gitlab::LDAP::Person do
}
}
)
- config = Gitlab::LDAP::Config.new('ldapmain')
+ config = Gitlab::Auth::LDAP::Config.new('ldapmain')
ldap_attributes = described_class.ldap_attributes(config)
expect(ldap_attributes).to match_array(%w(dn uid cn mail memberof))
diff --git a/spec/lib/gitlab/ldap/user_spec.rb b/spec/lib/gitlab/auth/ldap/user_spec.rb
index 048caa38fcf..cab2169593a 100644
--- a/spec/lib/gitlab/ldap/user_spec.rb
+++ b/spec/lib/gitlab/auth/ldap/user_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::LDAP::User do
+describe Gitlab::Auth::LDAP::User do
let(:ldap_user) { described_class.new(auth_hash) }
let(:gl_user) { ldap_user.gl_user }
let(:info) do
@@ -177,7 +177,7 @@ describe Gitlab::LDAP::User do
describe 'blocking' do
def configure_block(value)
- allow_any_instance_of(Gitlab::LDAP::Config)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config)
.to receive(:block_auto_created_users).and_return(value)
end
diff --git a/spec/lib/gitlab/o_auth/auth_hash_spec.rb b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
index dbcc200b90b..40001cea22e 100644
--- a/spec/lib/gitlab/o_auth/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/auth_hash_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::OAuth::AuthHash do
+describe Gitlab::Auth::OAuth::AuthHash do
let(:provider) { 'ldap'.freeze }
let(:auth_hash) do
described_class.new(
diff --git a/spec/lib/gitlab/o_auth/provider_spec.rb b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
index 30faf107e3f..fc35d430917 100644
--- a/spec/lib/gitlab/o_auth/provider_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/provider_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::OAuth::Provider do
+describe Gitlab::Auth::OAuth::Provider do
describe '#config_for' do
context 'for an LDAP provider' do
context 'when the provider exists' do
diff --git a/spec/lib/gitlab/o_auth/user_spec.rb b/spec/lib/gitlab/auth/o_auth/user_spec.rb
index b8455403bdb..0c71f1d8ca6 100644
--- a/spec/lib/gitlab/o_auth/user_spec.rb
+++ b/spec/lib/gitlab/auth/o_auth/user_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::OAuth::User do
+describe Gitlab::Auth::OAuth::User do
let(:oauth_user) { described_class.new(auth_hash) }
let(:gl_user) { oauth_user.gl_user }
let(:uid) { 'my-uid' }
@@ -18,7 +18,7 @@ describe Gitlab::OAuth::User do
}
}
end
- let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+ let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
describe '#persisted?' do
let!(:existing_user) { create(:omniauth_user, extern_uid: 'my-uid', provider: 'my-provider') }
@@ -39,7 +39,7 @@ describe Gitlab::OAuth::User do
describe '#save' do
def stub_ldap_config(messages)
- allow(Gitlab::LDAP::Config).to receive_messages(messages)
+ allow(Gitlab::Auth::LDAP::Config).to receive_messages(messages)
end
let(:provider) { 'twitter' }
@@ -215,7 +215,7 @@ describe Gitlab::OAuth::User do
context "and no account for the LDAP user" do
before do
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
oauth_user.save
end
@@ -250,7 +250,7 @@ describe Gitlab::OAuth::User do
context "and LDAP user has an account already" do
let!(:existing_user) { create(:omniauth_user, email: 'john@example.com', extern_uid: dn, provider: 'ldapmain', username: 'john') }
it "adds the omniauth identity to the LDAP account" do
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
oauth_user.save
@@ -270,8 +270,8 @@ describe Gitlab::OAuth::User do
context 'when an LDAP person is not found by uid' do
it 'tries to find an LDAP person by DN and adds the omniauth identity to the user' do
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil)
- allow(Gitlab::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).and_return(ldap_user)
oauth_user.save
@@ -297,7 +297,7 @@ describe Gitlab::OAuth::User do
context 'and no account for the LDAP user' do
it 'creates a user favoring the LDAP username and strips email domain' do
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
oauth_user.save
@@ -309,7 +309,7 @@ describe Gitlab::OAuth::User do
context "and no corresponding LDAP person" do
before do
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(nil)
end
include_examples "to verify compliance with allow_single_sign_on"
@@ -358,13 +358,13 @@ describe Gitlab::OAuth::User do
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { ['johndoe@example.com', 'john2@example.com'] }
allow(ldap_user).to receive(:dn) { dn }
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).and_return(ldap_user)
end
context "and no account for the LDAP user" do
context 'dont block on create (LDAP)' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false)
end
it do
@@ -376,7 +376,7 @@ describe Gitlab::OAuth::User do
context 'block on create (LDAP)' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true)
end
it do
@@ -392,7 +392,7 @@ describe Gitlab::OAuth::User do
context 'dont block on create (LDAP)' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false)
end
it do
@@ -404,7 +404,7 @@ describe Gitlab::OAuth::User do
context 'block on create (LDAP)' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true)
end
it do
@@ -448,7 +448,7 @@ describe Gitlab::OAuth::User do
context 'dont block on create (LDAP)' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: false)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: false)
end
it do
@@ -460,7 +460,7 @@ describe Gitlab::OAuth::User do
context 'block on create (LDAP)' do
before do
- allow_any_instance_of(Gitlab::LDAP::Config).to receive_messages(block_auto_created_users: true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Config).to receive_messages(block_auto_created_users: true)
end
it do
diff --git a/spec/lib/gitlab/saml/auth_hash_spec.rb b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
index a555935aea3..bb950e6bbf8 100644
--- a/spec/lib/gitlab/saml/auth_hash_spec.rb
+++ b/spec/lib/gitlab/auth/saml/auth_hash_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Saml::AuthHash do
+describe Gitlab::Auth::Saml::AuthHash do
include LoginHelpers
let(:raw_info_attr) { { 'groups' => %w(Developers Freelancers) } }
diff --git a/spec/lib/gitlab/saml/user_spec.rb b/spec/lib/gitlab/auth/saml/user_spec.rb
index 1765980e977..62514ca0688 100644
--- a/spec/lib/gitlab/saml/user_spec.rb
+++ b/spec/lib/gitlab/auth/saml/user_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Gitlab::Saml::User do
+describe Gitlab::Auth::Saml::User do
include LdapHelpers
include LoginHelpers
@@ -17,7 +17,7 @@ describe Gitlab::Saml::User do
email: 'john@mail.com'
}
end
- let(:ldap_user) { Gitlab::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
+ let(:ldap_user) { Gitlab::Auth::LDAP::Person.new(Net::LDAP::Entry.new, 'ldapmain') }
describe '#save' do
before do
@@ -159,10 +159,10 @@ describe Gitlab::Saml::User do
allow(ldap_user).to receive(:username) { uid }
allow(ldap_user).to receive(:email) { %w(john@mail.com john2@example.com) }
allow(ldap_user).to receive(:dn) { dn }
- allow(Gitlab::LDAP::Adapter).to receive(:new).and_return(adapter)
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
- allow(Gitlab::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user)
- allow(Gitlab::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Adapter).to receive(:new).and_return(adapter)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).with(uid, adapter).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_dn).with(dn, adapter).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_email).with('john@mail.com', adapter).and_return(ldap_user)
end
context 'and no account for the LDAP user' do
@@ -210,10 +210,10 @@ describe Gitlab::Saml::User do
nil_types = uid_types - [uid_type]
nil_types.each do |type|
- allow(Gitlab::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:"find_by_#{type}").and_return(nil)
end
- allow(Gitlab::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:"find_by_#{uid_type}").and_return(ldap_user)
end
it 'adds the omniauth identity to the LDAP account' do
@@ -280,7 +280,7 @@ describe Gitlab::Saml::User do
it 'adds the LDAP identity to the existing SAML user' do
create(:omniauth_user, email: 'john@mail.com', extern_uid: dn, provider: 'saml', username: 'john')
- allow(Gitlab::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user)
+ allow(Gitlab::Auth::LDAP::Person).to receive(:find_by_uid).with(dn, adapter).and_return(ldap_user)
local_hash = OmniAuth::AuthHash.new(uid: dn, provider: provider, info: info_hash)
local_saml_user = described_class.new(local_hash)
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index cc202ce8bca..f969f9e8e38 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -309,17 +309,17 @@ describe Gitlab::Auth do
context "with ldap enabled" do
before do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end
it "tries to autheticate with db before ldap" do
- expect(Gitlab::LDAP::Authentication).not_to receive(:login)
+ expect(Gitlab::Auth::LDAP::Authentication).not_to receive(:login)
gl_auth.find_with_user_password(username, password)
end
it "uses ldap as fallback to for authentication" do
- expect(Gitlab::LDAP::Authentication).to receive(:login)
+ expect(Gitlab::Auth::LDAP::Authentication).to receive(:login)
gl_auth.find_with_user_password('ldap_user', 'password')
end
@@ -336,7 +336,7 @@ describe Gitlab::Auth do
context "with ldap enabled" do
before do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
end
it "does not find non-ldap user by valid login/password" do
diff --git a/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
new file mode 100644
index 00000000000..e112e9e9e3d
--- /dev/null
+++ b/spec/lib/gitlab/background_migration/migrate_build_stage_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+describe Gitlab::BackgroundMigration::MigrateBuildStage, :migration, schema: 20180212101928 do
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+ let(:jobs) { table(:ci_builds) }
+
+ STATUSES = { created: 0, pending: 1, running: 2, success: 3,
+ failed: 4, canceled: 5, skipped: 6, manual: 7 }.freeze
+
+ before do
+ projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
+ pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
+
+ jobs.create!(id: 1, commit_id: 1, project_id: 123,
+ stage_idx: 2, stage: 'build', status: :success)
+ jobs.create!(id: 2, commit_id: 1, project_id: 123,
+ stage_idx: 2, stage: 'build', status: :success)
+ jobs.create!(id: 3, commit_id: 1, project_id: 123,
+ stage_idx: 1, stage: 'test', status: :failed)
+ jobs.create!(id: 4, commit_id: 1, project_id: 123,
+ stage_idx: 1, stage: 'test', status: :success)
+ jobs.create!(id: 5, commit_id: 1, project_id: 123,
+ stage_idx: 3, stage: 'deploy', status: :pending)
+ jobs.create!(id: 6, commit_id: 1, project_id: 123,
+ stage_idx: 3, stage: nil, status: :pending)
+ end
+
+ it 'correctly migrates builds stages' do
+ expect(stages.count).to be_zero
+
+ described_class.new.perform(1, 6)
+
+ expect(stages.count).to eq 3
+ expect(stages.all.pluck(:name)).to match_array %w[test build deploy]
+ expect(jobs.where(stage_id: nil)).to be_one
+ expect(jobs.find_by(stage_id: nil).id).to eq 6
+ expect(stages.all.pluck(:status)).to match_array [STATUSES[:success],
+ STATUSES[:failed],
+ STATUSES[:pending]]
+ end
+
+ it 'recovers from unique constraint violation only twice' do
+ allow(described_class::Migratable::Stage)
+ .to receive(:find_by).and_return(nil)
+
+ expect(described_class::Migratable::Stage)
+ .to receive(:find_by).exactly(3).times
+
+ expect { described_class.new.perform(1, 6) }
+ .to raise_error ActiveRecord::RecordNotUnique
+ end
+end
diff --git a/spec/lib/gitlab/checks/lfs_integrity_spec.rb b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
index 17756621221..7201e4f7bf6 100644
--- a/spec/lib/gitlab/checks/lfs_integrity_spec.rb
+++ b/spec/lib/gitlab/checks/lfs_integrity_spec.rb
@@ -2,23 +2,25 @@ require 'spec_helper'
describe Gitlab::Checks::LfsIntegrity do
include ProjectForksHelper
+
let(:project) { create(:project, :repository) }
- let(:newrev) { '54fcc214b94e78d7a41a9a8fe6d87a5e59500e51' }
+ let(:repository) { project.repository }
+ let(:newrev) do
+ operations = BareRepoOperations.new(repository.path)
+
+ # Create a commit not pointed at by any ref to emulate being in the
+ # pre-receive hook so that `--not --all` returns some objects
+ operations.commit_tree('8856a329dd38ca86dfb9ce5aa58a16d88cc119bd', "New LFS objects")
+ end
subject { described_class.new(project, newrev) }
describe '#objects_missing?' do
- let(:blob_object) { project.repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
-
- before do
- allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects) do |&lazy_block|
- lazy_block.call([blob_object.id])
- end
- end
+ let(:blob_object) { repository.blob_at_branch('lfs', 'files/lfs/lfs_object.iso') }
context 'with LFS not enabled' do
it 'skips integrity check' do
- expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
subject.objects_missing?
end
@@ -33,7 +35,7 @@ describe Gitlab::Checks::LfsIntegrity do
let(:newrev) { nil }
it 'skips integrity check' do
- expect_any_instance_of(Gitlab::Git::RevList).not_to receive(:new_objects)
+ expect_any_instance_of(Gitlab::Git::LfsChanges).not_to receive(:new_pointers)
expect(subject.objects_missing?).to be_falsey
end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
new file mode 100644
index 00000000000..019a2ed184d
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/equals_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Equals do
+ let(:left) { double('left') }
+ let(:right) { double('right') }
+
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('==', left, right))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is an operator' do
+ expect(described_class.type).to eq :operator
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns false when left and right are not equal' do
+ allow(left).to receive(:evaluate).and_return(1)
+ allow(right).to receive(:evaluate).and_return(2)
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate(VARIABLE: 3)).to eq false
+ end
+
+ it 'returns true when left and right are equal' do
+ allow(left).to receive(:evaluate).and_return(1)
+ allow(right).to receive(:evaluate).and_return(1)
+
+ operator = described_class.new(left, right)
+
+ expect(operator.evaluate(VARIABLE: 3)).to eq true
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb
new file mode 100644
index 00000000000..b5a59929e11
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/null_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Null do
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('null'))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is a value lexeme' do
+ expect(described_class.type).to eq :value
+ end
+ end
+
+ describe '#evaluate' do
+ it 'always evaluates to `nil`' do
+ expect(described_class.new('null').evaluate).to be_nil
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
new file mode 100644
index 00000000000..86234dfb9e5
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/string_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::String do
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('"my string"'))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is a value lexeme' do
+ expect(described_class.type).to eq :value
+ end
+ end
+
+ describe '.scan' do
+ context 'when using double quotes' do
+ it 'correctly identifies string token' do
+ scanner = StringScanner.new('"some string"')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate).to eq 'some string'
+ end
+ end
+
+ context 'when using single quotes' do
+ it 'correctly identifies string token' do
+ scanner = StringScanner.new("'some string 2'")
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate).to eq 'some string 2'
+ end
+ end
+
+ context 'when there are mixed quotes in the string' do
+ it 'is a greedy scanner for double quotes' do
+ scanner = StringScanner.new('"some string" "and another one"')
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate).to eq 'some string'
+ end
+
+ it 'is a greedy scanner for single quotes' do
+ scanner = StringScanner.new("'some string' 'and another one'")
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate).to eq 'some string'
+ end
+
+ it 'allows to use single quotes inside double quotes' do
+ scanner = StringScanner.new(%("some ' string"))
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate).to eq "some ' string"
+ end
+
+ it 'allow to use double quotes inside single quotes' do
+ scanner = StringScanner.new(%('some " string'))
+
+ token = described_class.scan(scanner)
+
+ expect(token).not_to be_nil
+ expect(token.build.evaluate).to eq 'some " string'
+ end
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns string value it is is present' do
+ string = described_class.new('my string')
+
+ expect(string.evaluate).to eq 'my string'
+ end
+
+ it 'returns an empty string if it is empty' do
+ string = described_class.new('')
+
+ expect(string.evaluate).to eq ''
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb
new file mode 100644
index 00000000000..599a5411881
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexeme/variable_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexeme::Variable do
+ describe '.build' do
+ it 'creates a new instance of the token' do
+ expect(described_class.build('$VARIABLE'))
+ .to be_a(described_class)
+ end
+ end
+
+ describe '.type' do
+ it 'is a value lexeme' do
+ expect(described_class.type).to eq :value
+ end
+ end
+
+ describe '#evaluate' do
+ it 'returns variable value if it is defined' do
+ variable = described_class.new('VARIABLE')
+
+ expect(variable.evaluate(VARIABLE: 'my variable'))
+ .to eq 'my variable'
+ end
+
+ it 'allows to use a string as a variable key too' do
+ variable = described_class.new('VARIABLE')
+
+ expect(variable.evaluate('VARIABLE' => 'my variable'))
+ .to eq 'my variable'
+ end
+
+ it 'returns nil if it is not defined' do
+ variable = described_class.new('VARIABLE')
+
+ expect(variable.evaluate(OTHER: 'variable')).to be_nil
+ end
+
+ it 'returns an empty string if it is empty' do
+ variable = described_class.new('VARIABLE')
+
+ expect(variable.evaluate(VARIABLE: '')).to eq ''
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
new file mode 100644
index 00000000000..230ceeb07f8
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/lexer_spec.rb
@@ -0,0 +1,70 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Lexer do
+ let(:token_class) do
+ Gitlab::Ci::Pipeline::Expression::Token
+ end
+
+ describe '#tokens' do
+ it 'tokenss single value' do
+ tokens = described_class.new('$VARIABLE').tokens
+
+ expect(tokens).to be_one
+ expect(tokens).to all(be_an_instance_of(token_class))
+ end
+
+ it 'does ignore whitespace characters' do
+ tokens = described_class.new("\t$VARIABLE ").tokens
+
+ expect(tokens).to be_one
+ expect(tokens).to all(be_an_instance_of(token_class))
+ end
+
+ it 'tokenss multiple values of the same token' do
+ tokens = described_class.new("$VARIABLE1 $VARIABLE2").tokens
+
+ expect(tokens.size).to eq 2
+ expect(tokens).to all(be_an_instance_of(token_class))
+ end
+
+ it 'tokenss multiple values with different tokens' do
+ tokens = described_class.new('$VARIABLE "text" "value"').tokens
+
+ expect(tokens.size).to eq 3
+ expect(tokens.first.value).to eq '$VARIABLE'
+ expect(tokens.second.value).to eq '"text"'
+ expect(tokens.third.value).to eq '"value"'
+ end
+
+ it 'tokenss tokens and operators' do
+ tokens = described_class.new('$VARIABLE == "text"').tokens
+
+ expect(tokens.size).to eq 3
+ expect(tokens.first.value).to eq '$VARIABLE'
+ expect(tokens.second.value).to eq '=='
+ expect(tokens.third.value).to eq '"text"'
+ end
+
+ it 'limits statement to specified amount of tokens' do
+ lexer = described_class.new("$V1 $V2 $V3 $V4", max_tokens: 3)
+
+ expect { lexer.tokens }
+ .to raise_error described_class::SyntaxError
+ end
+
+ it 'raises syntax error in case of finding unknown tokens' do
+ lexer = described_class.new('$V1 123 $V2')
+
+ expect { lexer.tokens }
+ .to raise_error described_class::SyntaxError
+ end
+ end
+
+ describe '#lexemes' do
+ it 'returns an array of syntax lexemes' do
+ lexer = described_class.new('$VAR "text"')
+
+ expect(lexer.lexemes).to eq %w[variable string]
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
new file mode 100644
index 00000000000..e8e6f585310
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/parser_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Parser do
+ describe '#tree' do
+ context 'when using operators' do
+ it 'returns a reverse descent parse tree' do
+ expect(described_class.seed('$VAR1 == "123" == $VAR2').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
+ end
+ end
+
+ context 'when using a single token' do
+ it 'returns a single token instance' do
+ expect(described_class.seed('$VAR').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
+ end
+ end
+
+ context 'when expression is empty' do
+ it 'returns a null token' do
+ expect(described_class.seed('').tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Null
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
new file mode 100644
index 00000000000..472a58599d8
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/statement_spec.rb
@@ -0,0 +1,85 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Statement do
+ let(:pipeline) { build(:ci_pipeline) }
+
+ subject do
+ described_class.new(text, pipeline)
+ end
+
+ before do
+ pipeline.variables.build([key: 'VARIABLE', value: 'my variable'])
+ end
+
+ describe '#parse_tree' do
+ context 'when expression is empty' do
+ let(:text) { '' }
+
+ it 'raises an error' do
+ expect { subject.parse_tree }
+ .to raise_error described_class::StatementError
+ end
+ end
+
+ context 'when expression grammar is incorrect' do
+ table = [
+ '$VAR "text"', # missing operator
+ '== "123"', # invalid right side
+ "'single quotes'", # single quotes string
+ '$VAR ==', # invalid right side
+ '12345', # unknown syntax
+ '' # empty statement
+ ]
+
+ table.each do |syntax|
+ it "raises an error when syntax is `#{syntax}`" do
+ expect { described_class.new(syntax, pipeline).parse_tree }
+ .to raise_error described_class::StatementError
+ end
+ end
+ end
+
+ context 'when expression grammar is correct' do
+ context 'when using an operator' do
+ let(:text) { '$VAR == "value"' }
+
+ it 'returns a reverse descent parse tree' do
+ expect(subject.parse_tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Equals
+ end
+ end
+
+ context 'when using a single token' do
+ let(:text) { '$VARIABLE' }
+
+ it 'returns a single token instance' do
+ expect(subject.parse_tree)
+ .to be_a Gitlab::Ci::Pipeline::Expression::Lexeme::Variable
+ end
+ end
+ end
+ end
+
+ describe '#evaluate' do
+ statements = [
+ ['$VARIABLE == "my variable"', true],
+ ["$VARIABLE == 'my variable'", true],
+ ['"my variable" == $VARIABLE', true],
+ ['$VARIABLE == null', false],
+ ['$VAR == null', true],
+ ['null == $VAR', true],
+ ['$VARIABLE', 'my variable'],
+ ['$VAR', nil]
+ ]
+
+ statements.each do |expression, value|
+ context "when using expression `#{expression}`" do
+ let(:text) { expression }
+
+ it "evaluates to `#{value.inspect}`" do
+ expect(subject.evaluate).to eq value
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
new file mode 100644
index 00000000000..6d7453f0de5
--- /dev/null
+++ b/spec/lib/gitlab/ci/pipeline/expression/token_spec.rb
@@ -0,0 +1,45 @@
+require 'spec_helper'
+
+describe Gitlab::Ci::Pipeline::Expression::Token do
+ let(:value) { '$VARIABLE' }
+ let(:lexeme) { Gitlab::Ci::Pipeline::Expression::Lexeme::Variable }
+
+ subject { described_class.new(value, lexeme) }
+
+ describe '#value' do
+ it 'returns raw token value' do
+ expect(subject.value).to eq value
+ end
+ end
+
+ describe '#lexeme' do
+ it 'returns raw token lexeme' do
+ expect(subject.lexeme).to eq lexeme
+ end
+ end
+
+ describe '#build' do
+ it 'delegates to lexeme after adding a value' do
+ expect(lexeme).to receive(:build)
+ .with(value, 'some', 'args')
+
+ subject.build('some', 'args')
+ end
+
+ it 'allows passing only required arguments' do
+ expect(subject.build).to be_an_instance_of(lexeme)
+ end
+ end
+
+ describe '#type' do
+ it 'delegates type query to the lexeme' do
+ expect(subject.type).to eq :value
+ end
+ end
+
+ describe '#to_lexeme' do
+ it 'returns raw lexeme syntax component name' do
+ expect(subject.to_lexeme).to eq 'variable'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/contributions_calendar_spec.rb b/spec/lib/gitlab/contributions_calendar_spec.rb
index 49a179ba875..167876ca158 100644
--- a/spec/lib/gitlab/contributions_calendar_spec.rb
+++ b/spec/lib/gitlab/contributions_calendar_spec.rb
@@ -11,7 +11,7 @@ describe Gitlab::ContributionsCalendar do
end
let(:public_project) do
- create(:project, :public) do |project|
+ create(:project, :public, :repository) do |project|
create(:project_member, user: contributor, project: project)
end
end
@@ -40,13 +40,13 @@ describe Gitlab::ContributionsCalendar do
described_class.new(contributor, current_user)
end
- def create_event(project, day, hour = 0)
+ def create_event(project, day, hour = 0, action = Event::CREATED, target_symbol = :issue)
@targets ||= {}
- @targets[project] ||= create(:issue, project: project, author: contributor)
+ @targets[project] ||= create(target_symbol, project: project, author: contributor)
Event.create!(
project: project,
- action: Event::CREATED,
+ action: action,
target: @targets[project],
author: contributor,
created_at: DateTime.new(day.year, day.month, day.day, hour)
@@ -71,6 +71,12 @@ describe Gitlab::ContributionsCalendar do
expect(calendar(contributor).activity_dates[today]).to eq(2)
end
+ it "counts the diff notes on merge request" do
+ create_event(public_project, today, 0, Event::COMMENTED, :diff_note_on_merge_request)
+
+ expect(calendar(contributor).activity_dates[today]).to eq(1)
+ end
+
context "when events fall under different dates depending on the time zone" do
before do
create_event(public_project, today, 1)
diff --git a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
index 3fe0493ed9b..8b07da11c5d 100644
--- a/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/base_event_fetcher_spec.rb
@@ -41,7 +41,7 @@ describe Gitlab::CycleAnalytics::BaseEventFetcher do
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
- create_merge_request_closing_issue(issue)
+ create_merge_request_closing_issue(user, project, issue)
end
end
end
diff --git a/spec/lib/gitlab/cycle_analytics/events_spec.rb b/spec/lib/gitlab/cycle_analytics/events_spec.rb
index 38a47a159e1..397dd4e5d2c 100644
--- a/spec/lib/gitlab/cycle_analytics/events_spec.rb
+++ b/spec/lib/gitlab/cycle_analytics/events_spec.rb
@@ -236,8 +236,8 @@ describe 'cycle analytics events' do
pipeline.run!
pipeline.succeed!
- merge_merge_requests_closing_issue(context)
- deploy_master
+ merge_merge_requests_closing_issue(user, project, context)
+ deploy_master(user, project)
end
it 'has the name' do
@@ -294,8 +294,8 @@ describe 'cycle analytics events' do
let!(:context) { create(:issue, project: project, created_at: 2.days.ago) }
before do
- merge_merge_requests_closing_issue(context)
- deploy_master
+ merge_merge_requests_closing_issue(user, project, context)
+ deploy_master(user, project)
end
it 'has the total time' do
@@ -334,7 +334,7 @@ describe 'cycle analytics events' do
def setup(context)
milestone = create(:milestone, project: project)
context.update(milestone: milestone)
- mr = create_merge_request_closing_issue(context, commit_message: "References #{context.to_reference}")
+ mr = create_merge_request_closing_issue(user, project, context, commit_message: "References #{context.to_reference}")
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
diff --git a/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
new file mode 100644
index 00000000000..56a316318cb
--- /dev/null
+++ b/spec/lib/gitlab/cycle_analytics/usage_data_spec.rb
@@ -0,0 +1,140 @@
+require 'spec_helper'
+
+describe Gitlab::CycleAnalytics::UsageData do
+ describe '#to_json' do
+ before do
+ Timecop.freeze do
+ user = create(:user, :admin)
+ projects = create_list(:project, 2, :repository)
+
+ projects.each_with_index do |project, time|
+ issue = create(:issue, project: project, created_at: (time + 1).hour.ago)
+
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+
+ milestone = create(:milestone, project: project)
+ mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
+ pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
+
+ create_cycle(user, project, issue, mr, milestone, pipeline)
+ deploy_master(user, project, environment: 'staging')
+ deploy_master(user, project)
+ end
+ end
+ end
+
+ shared_examples 'a valid usage data result' do
+ it 'returns the aggregated usage data of every selected project' do
+ result = subject.to_json
+
+ expect(result).to have_key(:avg_cycle_analytics)
+
+ CycleAnalytics::STAGES.each do |stage|
+ expect(result[:avg_cycle_analytics]).to have_key(stage)
+
+ stage_values = result[:avg_cycle_analytics][stage]
+ expected_values = expect_values_per_stage[stage]
+
+ expected_values.each_pair do |op, value|
+ expect(stage_values).to have_key(op)
+
+ if op == :missing
+ expect(stage_values[op]).to eq(value)
+ else
+ # delta is used because of git timings that Timecop does not stub
+ expect(stage_values[op].to_i).to be_within(5).of(value.to_i)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when using postgresql', :postgresql do
+ let(:expect_values_per_stage) do
+ {
+ issue: {
+ average: 5400,
+ sd: 2545,
+ missing: 0
+ },
+ plan: {
+ average: 2,
+ sd: 2,
+ missing: 0
+ },
+ code: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ test: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ review: {
+ average: 0,
+ sd: 0,
+ missing: 0
+ },
+ staging: {
+ average: 0,
+ sd: 0,
+ missing: 0
+ },
+ production: {
+ average: 5400,
+ sd: 2545,
+ missing: 0
+ }
+ }
+ end
+
+ it_behaves_like 'a valid usage data result'
+ end
+
+ context 'when using mysql', :mysql do
+ let(:expect_values_per_stage) do
+ {
+ issue: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ plan: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ code: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ test: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ review: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ staging: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ },
+ production: {
+ average: nil,
+ sd: 0,
+ missing: 2
+ }
+ }
+ end
+
+ it_behaves_like 'a valid usage data result'
+ end
+ end
+end
diff --git a/spec/lib/gitlab/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index 91c43f2bdc0..ee91decafad 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -16,7 +16,7 @@ describe Gitlab::DataBuilder::Build do
it { expect(data[:build_status]).to eq(build.status) }
it { expect(data[:build_allow_failure]).to eq(false) }
it { expect(data[:project_id]).to eq(build.project.id) }
- it { expect(data[:project_name]).to eq(build.project.name_with_namespace) }
+ it { expect(data[:project_name]).to eq(build.project.full_name) }
context 'commit author_url' do
context 'when no commit present' do
diff --git a/spec/lib/gitlab/database/median_spec.rb b/spec/lib/gitlab/database/median_spec.rb
new file mode 100644
index 00000000000..1b5e30089ce
--- /dev/null
+++ b/spec/lib/gitlab/database/median_spec.rb
@@ -0,0 +1,17 @@
+require 'spec_helper'
+
+describe Gitlab::Database::Median do
+ let(:dummy_class) do
+ Class.new do
+ include Gitlab::Database::Median
+ end
+ end
+
+ subject(:median) { dummy_class.new }
+
+ describe '#median_datetimes' do
+ it 'raises NotSupportedError', :mysql do
+ expect { median.median_datetimes(nil, nil, nil, :project_id) }.to raise_error(dummy_class::NotSupportedError, "partition_column is not supported for MySQL")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
index 031efcf1291..53899e00b53 100644
--- a/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
+++ b/spec/lib/gitlab/email/handler/create_note_handler_spec.rb
@@ -55,8 +55,8 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
expect { receiver.execute }.to raise_error(Gitlab::Email::InvalidNoteError)
end
- context 'because the note was commands only' do
- let!(:email_raw) { fixture_file("emails/commands_only_reply.eml") }
+ context 'because the note was update commands only' do
+ let!(:email_raw) { fixture_file("emails/update_commands_only_reply.eml") }
context 'and current user cannot update noteable' do
it 'raises a CommandsOnlyNoteError' do
@@ -70,13 +70,10 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
end
it 'does not raise an error' do
- expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
-
# One system note is created for the 'close' event
expect { receiver.execute }.to change { noteable.notes.count }.by(1)
expect(noteable.reload).to be_closed
- expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
end
@@ -85,15 +82,13 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
context 'when the note contains quick actions' do
let!(:email_raw) { fixture_file("emails/commands_in_reply.eml") }
- context 'and current user cannot update noteable' do
- it 'post a note and does not update the noteable' do
- expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
-
- # One system note is created for the new note
- expect { receiver.execute }.to change { noteable.notes.count }.by(1)
+ context 'and current user cannot update the noteable' do
+ it 'only executes the commands that the user can perform' do
+ expect { receiver.execute }
+ .to change { noteable.notes.user.count }.by(1)
+ .and change { user.todos_pending_count }.from(0).to(1)
expect(noteable.reload).to be_open
- expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
end
end
@@ -102,14 +97,14 @@ describe Gitlab::Email::Handler::CreateNoteHandler do
project.add_developer(user)
end
- it 'post a note and updates the noteable' do
+ it 'posts a note and updates the noteable' do
expect(TodoService.new.todo_exist?(noteable, user)).to be_falsy
- # One system note is created for the new note, one for the 'close' event
- expect { receiver.execute }.to change { noteable.notes.count }.by(2)
+ expect { receiver.execute }
+ .to change { noteable.notes.user.count }.by(1)
+ .and change { user.todos_pending_count }.from(0).to(1)
expect(noteable.reload).to be_closed
- expect(TodoService.new.todo_exist?(noteable, user)).to be_truthy
end
end
end
diff --git a/spec/lib/gitlab/git/blob_spec.rb b/spec/lib/gitlab/git/blob_spec.rb
index a6341cd509b..67d898e787e 100644
--- a/spec/lib/gitlab/git/blob_spec.rb
+++ b/spec/lib/gitlab/git/blob_spec.rb
@@ -500,4 +500,33 @@ describe Gitlab::Git::Blob, seed_helper: true do
end
end
end
+
+ describe '#load_all_data!' do
+ let(:full_data) { 'abcd' }
+ let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: 'abc') }
+
+ subject { blob.load_all_data!(repository) }
+
+ it 'loads missing data' do
+ expect(Gitlab::GitalyClient).to receive(:migrate)
+ .with(:git_blob_load_all_data).and_return(full_data)
+
+ subject
+
+ expect(blob.data).to eq(full_data)
+ end
+
+ context 'with a fully loaded blob' do
+ let(:blob) { Gitlab::Git::Blob.new(name: 'test', size: 4, data: full_data) }
+
+ it "doesn't perform any loading" do
+ expect(Gitlab::GitalyClient).not_to receive(:migrate)
+ .with(:git_blob_load_all_data)
+
+ subject
+
+ expect(blob.data).to eq(full_data)
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/branch_spec.rb b/spec/lib/gitlab/git/branch_spec.rb
index 708870060e7..a19155ed5b0 100644
--- a/spec/lib/gitlab/git/branch_spec.rb
+++ b/spec/lib/gitlab/git/branch_spec.rb
@@ -59,5 +59,69 @@ describe Gitlab::Git::Branch, seed_helper: true do
it { expect(branch.dereferenced_target.sha).to eq(SeedRepo::LastCommit::ID) }
end
+ context 'with active, stale and future branches' do
+ let(:repository) do
+ Gitlab::Git::Repository.new('default', TEST_MUTABLE_REPO_PATH, '')
+ end
+
+ let(:user) { create(:user) }
+ let(:committer) do
+ Gitlab::Git.committer_hash(email: user.email, name: user.name)
+ end
+ let(:params) do
+ parents = [repository.rugged.head.target]
+ tree = parents.first.tree
+
+ {
+ message: 'commit message',
+ author: committer,
+ committer: committer,
+ tree: tree,
+ parents: parents
+ }
+ end
+ let(:stale_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago - 5.days) { create_commit } }
+ let(:active_sha) { Timecop.freeze(Gitlab::Git::Branch::STALE_BRANCH_THRESHOLD.ago + 5.days) { create_commit } }
+ let(:future_sha) { Timecop.freeze(100.days.since) { create_commit } }
+
+ before do
+ repository.create_branch('stale-1', stale_sha)
+ repository.create_branch('active-1', active_sha)
+ repository.create_branch('future-1', future_sha)
+ end
+
+ after do
+ ensure_seeds
+ end
+
+ describe 'examine if the branch is active or stale' do
+ let(:stale_branch) { repository.find_branch('stale-1') }
+ let(:active_branch) { repository.find_branch('active-1') }
+ let(:future_branch) { repository.find_branch('future-1') }
+
+ describe '#active?' do
+ it { expect(stale_branch.active?).to be_falsey }
+ it { expect(active_branch.active?).to be_truthy }
+ it { expect(future_branch.active?).to be_truthy }
+ end
+
+ describe '#stale?' do
+ it { expect(stale_branch.stale?).to be_truthy }
+ it { expect(active_branch.stale?).to be_falsey }
+ it { expect(future_branch.stale?).to be_falsey }
+ end
+
+ describe '#state' do
+ it { expect(stale_branch.state).to eq(:stale) }
+ it { expect(active_branch.state).to eq(:active) }
+ it { expect(future_branch.state).to eq(:active) }
+ end
+ end
+ end
+
it { expect(repository.branches.size).to eq(SeedRepo::Repo::BRANCHES.size) }
+
+ def create_commit
+ repository.create_commit(params.merge(committer: committer.merge(time: Time.now)))
+ end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index 0b20a6349a2..a05feaac1ca 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -393,81 +393,111 @@ describe Gitlab::Git::Commit, seed_helper: true do
end
end
- describe '.extract_signature' do
- subject { described_class.extract_signature(repository, commit_id) }
-
- shared_examples '.extract_signature' do
- context 'when the commit is signed' do
- let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
-
- it 'returns signature and signed text' do
- signature, signed_text = subject
-
- expected_signature = <<~SIGNATURE
- -----BEGIN PGP SIGNATURE-----
- Version: GnuPG/MacGPG2 v2.0.22 (Darwin)
- Comment: GPGTools - https://gpgtools.org
+ shared_examples 'extracting commit signature' do
+ context 'when the commit is signed' do
+ let(:commit_id) { '0b4bc9a49b562e85de7cc9e834518ea6828729b9' }
+
+ it 'returns signature and signed text' do
+ signature, signed_text = subject
+
+ expected_signature = <<~SIGNATURE
+ -----BEGIN PGP SIGNATURE-----
+ Version: GnuPG/MacGPG2 v2.0.22 (Darwin)
+ Comment: GPGTools - https://gpgtools.org
+
+ iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0
+ Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+
+ mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar
+ TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v
+ hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy
+ ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc=
+ =j51i
+ -----END PGP SIGNATURE-----
+ SIGNATURE
+
+ expect(signature).to eq(expected_signature.chomp)
+ expect(signature).to be_a_binary_string
+
+ expected_signed_text = <<~SIGNED_TEXT
+ tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae
+ parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f
+ author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200
+ committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200
+
+ Feature added
+
+ Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
+ SIGNED_TEXT
+
+ expect(signed_text).to eq(expected_signed_text)
+ expect(signed_text).to be_a_binary_string
+ end
+ end
- iQEcBAABCgAGBQJTDvaZAAoJEGJ8X1ifRn8XfvYIAMuB0yrbTGo1BnOSoDfyrjb0
- Kw2EyUzvXYL72B63HMdJ+/0tlSDC6zONF3fc+bBD8z+WjQMTbwFNMRbSSy2rKEh+
- mdRybOP3xBIMGgEph0/kmWln39nmFQBsPRbZBWoU10VfI/ieJdEOgOphszgryRar
- TyS73dLBGE9y9NIININVaNISet9D9QeXFqc761CGjh4YIghvPpi+YihMWapGka6v
- hgKhX+hc5rj+7IEE0CXmlbYR8OYvAbAArc5vJD7UTxAY4Z7/l9d6Ydt9GQ25khfy
- ANFgltYzlR6evLFmDjssiP/mx/ZMN91AL0ueJ9nNGv411Mu2CUW+tDCaQf35mdc=
- =j51i
- -----END PGP SIGNATURE-----
- SIGNATURE
+ context 'when the commit has no signature' do
+ let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
- expect(signature).to eq(expected_signature.chomp)
- expect(signature).to be_a_binary_string
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
- expected_signed_text = <<~SIGNED_TEXT
- tree 22bfa2fbd217df24731f43ff43a4a0f8db759dae
- parent ae73cb07c9eeaf35924a10f713b364d32b2dd34f
- author Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200
- committer Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com> 1393489561 +0200
+ context 'when the commit cannot be found' do
+ let(:commit_id) { Gitlab::Git::BLANK_SHA }
- Feature added
+ it 'returns nil' do
+ expect(subject).to be_nil
+ end
+ end
- Signed-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>
- SIGNED_TEXT
+ context 'when the commit ID is invalid' do
+ let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' }
- expect(signed_text).to eq(expected_signed_text)
- expect(signed_text).to be_a_binary_string
- end
+ it 'raises ArgumentError' do
+ expect { subject }.to raise_error(ArgumentError)
end
+ end
+ end
- context 'when the commit has no signature' do
- let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6' }
-
- it 'returns nil' do
- expect(subject).to be_nil
+ describe '.extract_signature_lazily' do
+ shared_examples 'loading signatures in batch once' do
+ it 'fetches signatures in batch once' do
+ commit_ids = %w[0b4bc9a49b562e85de7cc9e834518ea6828729b9 4b4918a572fa86f9771e5ba40fbd48e1eb03e2c6]
+ signatures = commit_ids.map do |commit_id|
+ described_class.extract_signature_lazily(repository, commit_id)
end
- end
- context 'when the commit cannot be found' do
- let(:commit_id) { Gitlab::Git::BLANK_SHA }
+ expect(described_class).to receive(:batch_signature_extraction)
+ .with(repository, commit_ids)
+ .once
+ .and_return({})
- it 'returns nil' do
- expect(subject).to be_nil
- end
+ 2.times { signatures.each(&:itself) }
end
+ end
- context 'when the commit ID is invalid' do
- let(:commit_id) { '4b4918a572fa86f9771e5ba40fbd48e' }
+ subject { described_class.extract_signature_lazily(repository, commit_id).itself }
- it 'raises ArgumentError' do
- expect { subject }.to raise_error(ArgumentError)
- end
- end
+ context 'with Gitaly extract_commit_signature_in_batch feature enabled' do
+ it_behaves_like 'extracting commit signature'
+ it_behaves_like 'loading signatures in batch once'
+ end
+
+ context 'with Gitaly extract_commit_signature_in_batch feature disabled', :disable_gitaly do
+ it_behaves_like 'extracting commit signature'
+ it_behaves_like 'loading signatures in batch once'
end
+ end
+
+ describe '.extract_signature' do
+ subject { described_class.extract_signature(repository, commit_id) }
context 'with gitaly' do
- it_behaves_like '.extract_signature'
+ it_behaves_like 'extracting commit signature'
end
- context 'without gitaly', :skip_gitaly_mock do
- it_behaves_like '.extract_signature'
+ context 'without gitaly', :disable_gitaly do
+ it_behaves_like 'extracting commit signature'
end
end
end
diff --git a/spec/lib/gitlab/git/lfs_changes_spec.rb b/spec/lib/gitlab/git/lfs_changes_spec.rb
index c9007d7d456..d0dd8c6303f 100644
--- a/spec/lib/gitlab/git/lfs_changes_spec.rb
+++ b/spec/lib/gitlab/git/lfs_changes_spec.rb
@@ -7,34 +7,36 @@ describe Gitlab::Git::LfsChanges do
subject { described_class.new(project.repository, newrev) }
- describe 'new_pointers' do
- before do
- allow_any_instance_of(Gitlab::Git::RevList).to receive(:new_objects).and_yield([blob_object_id])
+ describe '#new_pointers' do
+ shared_examples 'new pointers' do
+ it 'filters new objects to find lfs pointers' do
+ expect(subject.new_pointers(not_in: []).first.id).to eq(blob_object_id)
+ end
+
+ it 'limits new_objects using object_limit' do
+ expect(subject.new_pointers(object_limit: 1)).to eq([])
+ end
end
- it 'uses rev-list to find new objects' do
- rev_list = double
- allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
-
- expect(rev_list).to receive(:new_objects).and_return([])
-
- subject.new_pointers
+ context 'with gitaly enabled' do
+ it_behaves_like 'new pointers'
end
- it 'filters new objects to find lfs pointers' do
- expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [blob_object_id])
+ context 'with gitaly disabled', :skip_gitaly_mock do
+ it_behaves_like 'new pointers'
- subject.new_pointers(object_limit: 1)
- end
+ it 'uses rev-list to find new objects' do
+ rev_list = double
+ allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
- it 'limits new_objects using object_limit' do
- expect(Gitlab::Git::Blob).to receive(:batch_lfs_pointers).with(project.repository, [])
+ expect(rev_list).to receive(:new_objects).and_return([])
- subject.new_pointers(object_limit: 0)
+ subject.new_pointers
+ end
end
end
- describe 'all_pointers' do
+ describe '#all_pointers', :skip_gitaly_mock do
it 'uses rev-list to find all objects' do
rev_list = double
allow(Gitlab::Git::RevList).to receive(:new).and_return(rev_list)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index d601a383a98..52c9876cbb6 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -751,245 +751,263 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#log" do
- let(:commit_with_old_name) do
- Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id)
- end
- let(:commit_with_new_name) do
- Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id)
- end
- let(:rename_commit) do
- Gitlab::Git::Commit.decorate(repository, @rename_commit_id)
- end
-
- before(:context) do
- # Add new commits so that there's a renamed file in the commit history
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
- @commit_with_old_name_id = new_commit_edit_old_file(repo)
- @rename_commit_id = new_commit_move_file(repo)
- @commit_with_new_name_id = new_commit_edit_new_file(repo)
- end
-
- after(:context) do
- # Erase our commits so other tests get the original repo
- repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
- repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
- end
-
- context "where 'follow' == true" do
- let(:options) { { ref: "master", follow: true } }
+ shared_examples 'repository log' do
+ let(:commit_with_old_name) do
+ Gitlab::Git::Commit.decorate(repository, @commit_with_old_name_id)
+ end
+ let(:commit_with_new_name) do
+ Gitlab::Git::Commit.decorate(repository, @commit_with_new_name_id)
+ end
+ let(:rename_commit) do
+ Gitlab::Git::Commit.decorate(repository, @rename_commit_id)
+ end
- context "and 'path' is a directory" do
- it "does not follow renames" do
- log_commits = repository.log(options.merge(path: "encoding"))
+ before(:context) do
+ # Add new commits so that there's a renamed file in the commit history
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ @commit_with_old_name_id = new_commit_edit_old_file(repo)
+ @rename_commit_id = new_commit_move_file(repo)
+ @commit_with_new_name_id = new_commit_edit_new_file(repo)
+ end
- aggregate_failures do
- expect(log_commits).to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_old_name)
- end
- end
+ after(:context) do
+ # Erase our commits so other tests get the original repo
+ repo = Gitlab::Git::Repository.new('default', TEST_REPO_PATH, '').rugged
+ repo.references.update("refs/heads/master", SeedRepo::LastCommit::ID)
end
- context "and 'path' is a file that matches the new filename" do
- context 'without offset' do
- it "follows renames" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
+ context "where 'follow' == true" do
+ let(:options) { { ref: "master", follow: true } }
+
+ context "and 'path' is a directory" do
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "encoding"))
aggregate_failures do
expect(log_commits).to include(commit_with_new_name)
expect(log_commits).to include(rename_commit)
- expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).not_to include(commit_with_old_name)
end
end
end
- context 'with offset=1' do
- it "follows renames and skip the latest commit" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
+ context "and 'path' is a file that matches the new filename" do
+ context 'without offset' do
+ it "follows renames" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG"))
- aggregate_failures do
- expect(log_commits).not_to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).to include(commit_with_old_name)
+ aggregate_failures do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
end
end
- end
- context 'with offset=1', 'and limit=1' do
- it "follows renames, skip the latest commit and return only one commit" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
+ context 'with offset=1' do
+ it "follows renames and skip the latest commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1))
- expect(log_commits).to contain_exactly(rename_commit)
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
end
- end
- context 'with offset=1', 'and limit=2' do
- it "follows renames, skip the latest commit and return only two commits" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
+ context 'with offset=1', 'and limit=1' do
+ it "follows renames, skip the latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 1))
- aggregate_failures do
- expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
+ expect(log_commits).to contain_exactly(rename_commit)
end
end
- end
- context 'with offset=2' do
- it "follows renames and skip the latest commit" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
+ context 'with offset=1', 'and limit=2' do
+ it "follows renames, skip the latest commit and return only two commits" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 1, limit: 2))
- aggregate_failures do
- expect(log_commits).not_to include(commit_with_new_name)
- expect(log_commits).not_to include(rename_commit)
- expect(log_commits).to include(commit_with_old_name)
+ aggregate_failures do
+ expect(log_commits).to contain_exactly(rename_commit, commit_with_old_name)
+ end
+ end
+ end
+
+ context 'with offset=2' do
+ it "follows renames and skip the latest commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
end
end
- end
- context 'with offset=2', 'and limit=1' do
- it "follows renames, skip the two latest commit and return only one commit" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
+ context 'with offset=2', 'and limit=1' do
+ it "follows renames, skip the two latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 1))
- expect(log_commits).to contain_exactly(commit_with_old_name)
+ expect(log_commits).to contain_exactly(commit_with_old_name)
+ end
+ end
+
+ context 'with offset=2', 'and limit=2' do
+ it "follows renames, skip the two latest commit and return only one commit" do
+ log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
+
+ aggregate_failures do
+ expect(log_commits).not_to include(commit_with_new_name)
+ expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(commit_with_old_name)
+ end
+ end
end
end
- context 'with offset=2', 'and limit=2' do
- it "follows renames, skip the two latest commit and return only one commit" do
- log_commits = repository.log(options.merge(path: "encoding/CHANGELOG", offset: 2, limit: 2))
+ context "and 'path' is a file that matches the old filename" do
+ it "does not follow renames" do
+ log_commits = repository.log(options.merge(path: "CHANGELOG"))
aggregate_failures do
expect(log_commits).not_to include(commit_with_new_name)
- expect(log_commits).not_to include(rename_commit)
+ expect(log_commits).to include(rename_commit)
expect(log_commits).to include(commit_with_old_name)
end
end
end
- end
- context "and 'path' is a file that matches the old filename" do
- it "does not follow renames" do
- log_commits = repository.log(options.merge(path: "CHANGELOG"))
+ context "unknown ref" do
+ it "returns an empty array" do
+ log_commits = repository.log(options.merge(ref: 'unknown'))
- aggregate_failures do
- expect(log_commits).not_to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).to eq([])
end
end
end
- context "unknown ref" do
- it "returns an empty array" do
- log_commits = repository.log(options.merge(ref: 'unknown'))
+ context "where 'follow' == false" do
+ options = { follow: false }
- expect(log_commits).to eq([])
- end
- end
- end
-
- context "where 'follow' == false" do
- options = { follow: false }
+ context "and 'path' is a directory" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding"))
+ end
- context "and 'path' is a directory" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding"))
+ it "does not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
end
- it "should not follow renames" do
- expect(log_commits).to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_old_name)
- end
- end
+ context "and 'path' is a file that matches the new filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "encoding/CHANGELOG"))
+ end
- context "and 'path' is a file that matches the new filename" do
- let(:log_commits) do
- repository.log(options.merge(path: "encoding/CHANGELOG"))
+ it "does not follow renames" do
+ expect(log_commits).to include(commit_with_new_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_old_name)
+ end
end
- it "should not follow renames" do
- expect(log_commits).to include(commit_with_new_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_old_name)
- end
- end
+ context "and 'path' is a file that matches the old filename" do
+ let(:log_commits) do
+ repository.log(options.merge(path: "CHANGELOG"))
+ end
- context "and 'path' is a file that matches the old filename" do
- let(:log_commits) do
- repository.log(options.merge(path: "CHANGELOG"))
+ it "does not follow renames" do
+ expect(log_commits).to include(commit_with_old_name)
+ expect(log_commits).to include(rename_commit)
+ expect(log_commits).not_to include(commit_with_new_name)
+ end
end
- it "should not follow renames" do
- expect(log_commits).to include(commit_with_old_name)
- expect(log_commits).to include(rename_commit)
- expect(log_commits).not_to include(commit_with_new_name)
+ context "and 'path' includes a directory that used to be a file" do
+ let(:log_commits) do
+ repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
+ end
+
+ it "returns a list of commits" do
+ expect(log_commits.size).to eq(1)
+ end
end
end
- context "and 'path' includes a directory that used to be a file" do
- let(:log_commits) do
- repository.log(options.merge(ref: "refs/heads/fix-blob-path", path: "files/testdir/file.txt"))
- end
+ context "where provides 'after' timestamp" do
+ options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
- it "should return a list of commits" do
- expect(log_commits.size).to eq(1)
+ it "should returns commits on or after that timestamp" do
+ commits = repository.log(options)
+
+ expect(commits.size).to be > 0
+ expect(commits).to satisfy do |commits|
+ commits.all? { |commit| commit.committed_date >= options[:after] }
+ end
end
end
- end
- context "where provides 'after' timestamp" do
- options = { after: Time.iso8601('2014-03-03T20:15:01+00:00') }
+ context "where provides 'before' timestamp" do
+ options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
- it "should returns commits on or after that timestamp" do
- commits = repository.log(options)
+ it "should returns commits on or before that timestamp" do
+ commits = repository.log(options)
- expect(commits.size).to be > 0
- expect(commits).to satisfy do |commits|
- commits.all? { |commit| commit.committed_date >= options[:after] }
+ expect(commits.size).to be > 0
+ expect(commits).to satisfy do |commits|
+ commits.all? { |commit| commit.committed_date <= options[:before] }
+ end
end
end
- end
- context "where provides 'before' timestamp" do
- options = { before: Time.iso8601('2014-03-03T20:15:01+00:00') }
+ context 'when multiple paths are provided' do
+ let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
+
+ def commit_files(commit)
+ commit.rugged_diff_from_parent.deltas.flat_map do |delta|
+ [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ end
+ end
- it "should returns commits on or before that timestamp" do
- commits = repository.log(options)
+ it 'only returns commits matching at least one path' do
+ commits = repository.log(options)
- expect(commits.size).to be > 0
- expect(commits).to satisfy do |commits|
- commits.all? { |commit| commit.committed_date <= options[:before] }
+ expect(commits.size).to be > 0
+ expect(commits).to satisfy do |commits|
+ commits.none? { |commit| (commit_files(commit) & options[:path]).empty? }
+ end
end
end
- end
- context 'when multiple paths are provided' do
- let(:options) { { ref: 'master', path: ['PROCESS.md', 'README.md'] } }
+ context 'limit validation' do
+ where(:limit) do
+ [0, nil, '', 'foo']
+ end
- def commit_files(commit)
- commit.rugged_diff_from_parent.deltas.flat_map do |delta|
- [delta.old_file[:path], delta.new_file[:path]].uniq.compact
+ with_them do
+ it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) }
end
end
- it 'only returns commits matching at least one path' do
- commits = repository.log(options)
+ context 'with all' do
+ it 'returns a list of commits' do
+ commits = repository.log({ all: true, limit: 50 })
- expect(commits.size).to be > 0
- expect(commits).to satisfy do |commits|
- commits.none? { |commit| (commit_files(commit) & options[:path]).empty? }
+ expect(commits.size).to eq(37)
end
end
end
- context 'limit validation' do
- where(:limit) do
- [0, nil, '', 'foo']
- end
+ context 'when Gitaly find_commits feature is enabled' do
+ it_behaves_like 'repository log'
+ end
- with_them do
- it { expect { repository.log(limit: limit) }.to raise_error(ArgumentError) }
- end
+ context 'when Gitaly find_commits feature is disabled', :disable_gitaly do
+ it_behaves_like 'repository log'
end
end
@@ -1126,13 +1144,27 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(repository.count_commits(options)).to eq(10)
end
end
+
+ context "with all" do
+ it "returns the number of commits in the whole repository" do
+ options = { all: true }
+
+ expect(repository.count_commits(options)).to eq(34)
+ end
+ end
+
+ context 'without all or ref being specified' do
+ it "raises an ArgumentError" do
+ expect { repository.count_commits({}) }.to raise_error(ArgumentError)
+ end
+ end
end
context 'when Gitaly count_commits feature is enabled' do
it_behaves_like 'extended commit counting'
end
- context 'when Gitaly count_commits feature is disabled', :skip_gitaly_mock do
+ context 'when Gitaly count_commits feature is disabled', :disable_gitaly do
it_behaves_like 'extended commit counting'
end
end
@@ -1406,79 +1438,95 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe "#copy_gitattributes" do
- let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
-
- it "raises an error with invalid ref" do
- expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
- end
+ shared_examples 'applying git attributes' do
+ let(:attributes_path) { File.join(SEED_STORAGE_PATH, TEST_REPO_PATH, 'info/attributes') }
- context "with no .gitattrbutes" do
- before do
- repository.copy_gitattributes("master")
+ after do
+ FileUtils.rm_rf(attributes_path) if Dir.exist?(attributes_path)
end
- it "does not have an info/attributes" do
- expect(File.exist?(attributes_path)).to be_falsey
+ it "raises an error with invalid ref" do
+ expect { repository.copy_gitattributes("invalid") }.to raise_error(Gitlab::Git::Repository::InvalidRef)
end
- after do
- FileUtils.rm_rf(attributes_path)
- end
- end
+ context 'when forcing encoding issues' do
+ let(:branch_name) { "ʕ•ᴥ•ʔ" }
- context "with .gitattrbutes" do
- before do
- repository.copy_gitattributes("gitattributes")
- end
+ before do
+ repository.create_branch(branch_name, "master")
+ end
- it "has an info/attributes" do
- expect(File.exist?(attributes_path)).to be_truthy
- end
+ after do
+ repository.rm_branch(branch_name, user: build(:admin))
+ end
- it "has the same content in info/attributes as .gitattributes" do
- contents = File.open(attributes_path, "rb") { |f| f.read }
- expect(contents).to eq("*.md binary\n")
- end
+ it "doesn't raise with a valid unicode ref" do
+ expect { repository.copy_gitattributes(branch_name) }.not_to raise_error
- after do
- FileUtils.rm_rf(attributes_path)
+ repository
+ end
end
- end
- context "with updated .gitattrbutes" do
- before do
- repository.copy_gitattributes("gitattributes")
- repository.copy_gitattributes("gitattributes-updated")
- end
+ context "with no .gitattrbutes" do
+ before do
+ repository.copy_gitattributes("master")
+ end
- it "has an info/attributes" do
- expect(File.exist?(attributes_path)).to be_truthy
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
end
- it "has the updated content in info/attributes" do
- contents = File.read(attributes_path)
- expect(contents).to eq("*.txt binary\n")
- end
+ context "with .gitattrbutes" do
+ before do
+ repository.copy_gitattributes("gitattributes")
+ end
- after do
- FileUtils.rm_rf(attributes_path)
- end
- end
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
- context "with no .gitattrbutes in HEAD but with previous info/attributes" do
- before do
- repository.copy_gitattributes("gitattributes")
- repository.copy_gitattributes("master")
+ it "has the same content in info/attributes as .gitattributes" do
+ contents = File.open(attributes_path, "rb") { |f| f.read }
+ expect(contents).to eq("*.md binary\n")
+ end
end
- it "does not have an info/attributes" do
- expect(File.exist?(attributes_path)).to be_falsey
+ context "with updated .gitattrbutes" do
+ before do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("gitattributes-updated")
+ end
+
+ it "has an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_truthy
+ end
+
+ it "has the updated content in info/attributes" do
+ contents = File.read(attributes_path)
+ expect(contents).to eq("*.txt binary\n")
+ end
end
- after do
- FileUtils.rm_rf(attributes_path)
+ context "with no .gitattrbutes in HEAD but with previous info/attributes" do
+ before do
+ repository.copy_gitattributes("gitattributes")
+ repository.copy_gitattributes("master")
+ end
+
+ it "does not have an info/attributes" do
+ expect(File.exist?(attributes_path)).to be_falsey
+ end
end
end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like 'applying git attributes'
+ end
+
+ context 'when gitaly is disabled', :disable_gitaly do
+ it_behaves_like 'applying git attributes'
+ end
end
describe '#ref_exists?' do
@@ -1649,6 +1697,35 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
end
+ describe '#license_short_name' do
+ shared_examples 'acquiring the Licensee license key' do
+ subject { repository.license_short_name }
+
+ context 'when no license file can be found' do
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository.raw_repository }
+
+ before do
+ project.repository.delete_file(project.owner, 'LICENSE', message: 'remove license', branch_name: 'master')
+ end
+
+ it { is_expected.to be_nil }
+ end
+
+ context 'when an mit license is found' do
+ it { is_expected.to eq('mit') }
+ end
+ end
+
+ context 'when gitaly is enabled' do
+ it_behaves_like 'acquiring the Licensee license key'
+ end
+
+ context 'when gitaly is disabled', :disable_gitaly do
+ it_behaves_like 'acquiring the Licensee license key'
+ end
+ end
+
describe '#with_repo_branch_commit' do
context 'when comparing with the same repository' do
let(:start_repository) { repository }
@@ -2283,6 +2360,20 @@ describe Gitlab::Git::Repository, seed_helper: true do
expect(subject).to match(/\h{40}/)
end
end
+
+ context 'with trailing whitespace in an invalid patch', :skip_gitaly_mock do
+ let(:diff) { "diff --git a/README.md b/README.md\nindex faaf198..43c5edf 100644\n--- a/README.md\n+++ b/README.md\n@@ -1,4 +1,4 @@\n-testme\n+ \n ====== \n \n Sample repo for testing gitlab features\n" }
+
+ it 'does not include whitespace warnings in the error' do
+ allow(repository).to receive(:run_git!).and_call_original
+ allow(repository).to receive(:run_git!).with(%W(diff --binary #{start_sha}...#{end_sha})).and_return(diff.force_encoding('ASCII-8BIT'))
+
+ expect { subject }.to raise_error do |error|
+ expect(error).to be_a(described_class::GitError)
+ expect(error.message).not_to include('trailing whitespace')
+ end
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb
index 19d3f55501e..6f07e423c1b 100644
--- a/spec/lib/gitlab/git_access_spec.rb
+++ b/spec/lib/gitlab/git_access_spec.rb
@@ -534,6 +534,19 @@ describe Gitlab::GitAccess do
expect { pull_access_check }.to raise_unauthorized('Your account has been blocked.')
end
+ context 'when the project repository does not exist' do
+ it 'returns not found' do
+ project.add_guest(user)
+ repo = project.repository
+ FileUtils.rm_rf(repo.path)
+
+ # Sanity check for rm_rf
+ expect(repo.exists?).to eq(false)
+
+ expect { pull_access_check }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.')
+ end
+ end
+
describe 'without access to project' do
context 'pull code' do
it { expect { pull_access_check }.to raise_not_found }
diff --git a/spec/lib/gitlab/git_access_wiki_spec.rb b/spec/lib/gitlab/git_access_wiki_spec.rb
index 215f1ecc9c5..730ede99fc9 100644
--- a/spec/lib/gitlab/git_access_wiki_spec.rb
+++ b/spec/lib/gitlab/git_access_wiki_spec.rb
@@ -57,7 +57,7 @@ describe Gitlab::GitAccessWiki do
# Sanity check for rm_rf
expect(wiki_repo.exists?).to eq(false)
- expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'A repository for this project does not exist yet.')
+ expect { subject }.to raise_error(Gitlab::GitAccess::NotFoundError, 'A repository for this project does not exist yet.')
end
end
end
diff --git a/spec/lib/gitlab/gitaly_client/blob_service_spec.rb b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
new file mode 100644
index 00000000000..a2770ef2fe4
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/blob_service_spec.rb
@@ -0,0 +1,60 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::BlobService do
+ let(:project) { create(:project, :repository) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.disk_path + '.git' }
+ let(:repository) { project.repository }
+ let(:client) { described_class.new(repository) }
+
+ describe '#get_new_lfs_pointers' do
+ let(:revision) { 'master' }
+ let(:limit) { 5 }
+ let(:not_in) { ['branch-a', 'branch-b'] }
+ let(:expected_params) do
+ { revision: revision, limit: limit, not_in_refs: not_in, not_in_all: false }
+ end
+
+ subject { client.get_new_lfs_pointers(revision, limit, not_in) }
+
+ it 'sends a get_new_lfs_pointers message' do
+ expect_any_instance_of(Gitaly::BlobService::Stub)
+ .to receive(:get_new_lfs_pointers)
+ .with(gitaly_request_with_params(expected_params), kind_of(Hash))
+ .and_return([])
+
+ subject
+ end
+
+ context 'with not_in = :all' do
+ let(:not_in) { :all }
+ let(:expected_params) do
+ { revision: revision, limit: limit, not_in_refs: [], not_in_all: true }
+ end
+
+ it 'sends the correct message' do
+ expect_any_instance_of(Gitaly::BlobService::Stub)
+ .to receive(:get_new_lfs_pointers)
+ .with(gitaly_request_with_params(expected_params), kind_of(Hash))
+ .and_return([])
+
+ subject
+ end
+ end
+ end
+
+ describe '#get_all_lfs_pointers' do
+ let(:revision) { 'master' }
+
+ subject { client.get_all_lfs_pointers(revision) }
+
+ it 'sends a get_all_lfs_pointers message' do
+ expect_any_instance_of(Gitaly::BlobService::Stub)
+ .to receive(:get_all_lfs_pointers)
+ .with(gitaly_request_with_params(revision: revision), kind_of(Hash))
+ .and_return([])
+
+ subject
+ end
+ end
+end
diff --git a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
index 001c4d3e10a..9be3fa633a7 100644
--- a/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/commit_service_spec.rb
@@ -113,7 +113,7 @@ describe Gitlab::GitalyClient::CommitService do
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return([])
- client.tree_entries(repository, revision, path)
+ client.tree_entries(repository, revision, path, false)
end
context 'with UTF-8 params strings' do
@@ -126,7 +126,7 @@ describe Gitlab::GitalyClient::CommitService do
.with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
.and_return([])
- client.tree_entries(repository, revision, path)
+ client.tree_entries(repository, revision, path, false)
end
end
end
diff --git a/spec/lib/gitlab/gpg/commit_spec.rb b/spec/lib/gitlab/gpg/commit_spec.rb
index 67c62458f0f..8c6d673391b 100644
--- a/spec/lib/gitlab/gpg/commit_spec.rb
+++ b/spec/lib/gitlab/gpg/commit_spec.rb
@@ -38,7 +38,7 @@ describe Gitlab::Gpg::Commit do
end
before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
@@ -101,7 +101,7 @@ describe Gitlab::Gpg::Commit do
end
before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
@@ -140,7 +140,7 @@ describe Gitlab::Gpg::Commit do
end
before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
@@ -175,7 +175,7 @@ describe Gitlab::Gpg::Commit do
end
before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
@@ -211,7 +211,7 @@ describe Gitlab::Gpg::Commit do
end
before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
@@ -241,7 +241,7 @@ describe Gitlab::Gpg::Commit do
let!(:commit) { create :commit, project: project, sha: commit_sha }
before do
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(
[
diff --git a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
index c034eccf2a6..6fbffc38444 100644
--- a/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
+++ b/spec/lib/gitlab/gpg/invalid_gpg_signature_updater_spec.rb
@@ -26,7 +26,7 @@ RSpec.describe Gitlab::Gpg::InvalidGpgSignatureUpdater do
before do
allow_any_instance_of(Project).to receive(:commit).and_return(commit)
- allow(Gitlab::Git::Commit).to receive(:extract_signature)
+ allow(Gitlab::Git::Commit).to receive(:extract_signature_lazily)
.with(Gitlab::Git::Repository, commit_sha)
.and_return(signature)
end
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 41a55027f4d..b20cc34dd5c 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -277,6 +277,7 @@ project:
- fork_network
- custom_attributes
- lfs_file_locks
+- project_badges
award_emoji:
- awardable
- user
@@ -293,3 +294,5 @@ issue_assignees:
- assignee
lfs_file_locks:
- user
+project_badges:
+- project
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index b6c1f0c81cb..62ef93f847a 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -14,8 +14,7 @@
"template": false,
"description": "",
"type": "ProjectLabel",
- "priorities": [
- ]
+ "priorities": []
},
{
"id": 3,
@@ -160,9 +159,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 352,
@@ -184,9 +181,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 353,
@@ -208,9 +203,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 354,
@@ -232,9 +225,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 355,
@@ -256,9 +247,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 356,
@@ -280,9 +269,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 357,
@@ -304,9 +291,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 358,
@@ -328,9 +313,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -395,9 +378,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 360,
@@ -419,9 +400,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 361,
@@ -443,9 +422,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 362,
@@ -467,9 +444,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 363,
@@ -491,9 +466,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 364,
@@ -515,9 +488,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 365,
@@ -539,9 +510,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 366,
@@ -563,9 +532,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -628,9 +595,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 368,
@@ -652,9 +617,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 369,
@@ -676,9 +639,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 370,
@@ -700,9 +661,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 371,
@@ -724,9 +683,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 372,
@@ -748,9 +705,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 373,
@@ -772,9 +727,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 374,
@@ -796,9 +749,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -840,9 +791,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 376,
@@ -864,9 +813,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 377,
@@ -888,9 +835,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 378,
@@ -912,9 +857,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 379,
@@ -936,9 +879,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 380,
@@ -960,9 +901,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 381,
@@ -984,9 +923,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 382,
@@ -1008,9 +945,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1052,9 +987,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 384,
@@ -1076,9 +1009,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 385,
@@ -1100,9 +1031,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 386,
@@ -1124,9 +1053,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 387,
@@ -1148,9 +1075,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 388,
@@ -1172,9 +1097,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 389,
@@ -1196,9 +1119,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 390,
@@ -1220,9 +1141,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1264,9 +1183,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 392,
@@ -1288,9 +1205,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 393,
@@ -1312,9 +1227,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 394,
@@ -1336,9 +1249,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 395,
@@ -1360,9 +1271,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 396,
@@ -1384,9 +1293,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 397,
@@ -1408,9 +1315,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 398,
@@ -1432,9 +1337,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1476,9 +1379,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 400,
@@ -1500,9 +1401,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 401,
@@ -1524,9 +1423,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 402,
@@ -1548,9 +1445,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 403,
@@ -1572,9 +1467,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 404,
@@ -1596,9 +1489,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 405,
@@ -1620,9 +1511,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 406,
@@ -1644,9 +1533,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1688,9 +1575,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 408,
@@ -1712,9 +1597,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 409,
@@ -1736,9 +1619,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 410,
@@ -1760,9 +1641,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 411,
@@ -1784,9 +1663,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 412,
@@ -1808,9 +1685,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 413,
@@ -1832,9 +1707,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 414,
@@ -1856,9 +1729,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -1900,9 +1771,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 416,
@@ -1924,9 +1793,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 417,
@@ -1948,9 +1815,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 418,
@@ -1972,9 +1837,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 419,
@@ -1996,9 +1859,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 420,
@@ -2020,9 +1881,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 421,
@@ -2044,9 +1903,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 422,
@@ -2068,9 +1925,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
},
@@ -2112,9 +1967,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 424,
@@ -2136,9 +1989,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 425,
@@ -2160,9 +2011,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 426,
@@ -2184,9 +2033,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 427,
@@ -2208,9 +2055,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 428,
@@ -2232,9 +2077,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 429,
@@ -2256,9 +2099,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 430,
@@ -2280,9 +2121,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
]
}
@@ -2378,12 +2217,8 @@
]
}
],
- "snippets": [
-
- ],
- "releases": [
-
- ],
+ "snippets": [],
+ "releases": [],
"project_members": [
{
"id": 36,
@@ -2515,9 +2350,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 672,
@@ -2539,9 +2372,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 673,
@@ -2563,9 +2394,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 674,
@@ -2587,9 +2416,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 675,
@@ -2611,9 +2438,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 676,
@@ -2635,9 +2460,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 677,
@@ -2659,9 +2482,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 678,
@@ -2683,9 +2504,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -2696,7 +2515,7 @@
"merge_request_diff_id": 27,
"relative_order": 0,
"sha": "bb5206fee213d983da88c47f9cf4cc6caf9c66dc",
- "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Feature conflcit added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-08-06T08:35:52.000+02:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2708,7 +2527,7 @@
"merge_request_diff_id": 27,
"relative_order": 1,
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2720,7 +2539,7 @@
"merge_request_diff_id": 27,
"relative_order": 2,
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2732,7 +2551,7 @@
"merge_request_diff_id": 27,
"relative_order": 3,
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
- "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2744,7 +2563,7 @@
"merge_request_diff_id": 27,
"relative_order": 4,
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
- "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2756,7 +2575,7 @@
"merge_request_diff_id": 27,
"relative_order": 5,
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
- "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -2834,7 +2653,7 @@
{
"merge_request_diff_id": 27,
"relative_order": 5,
- "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -2958,9 +2777,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 680,
@@ -2982,9 +2799,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 681,
@@ -3006,9 +2821,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 682,
@@ -3030,9 +2843,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 683,
@@ -3054,9 +2865,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 684,
@@ -3078,9 +2887,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 685,
@@ -3102,9 +2909,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 686,
@@ -3126,9 +2931,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -3139,7 +2942,7 @@
"merge_request_diff_id": 26,
"sha": "0b4bc9a49b562e85de7cc9e834518ea6828729b9",
"relative_order": 0,
- "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Feature added\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:26:01.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3237,9 +3040,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 778,
@@ -3261,9 +3062,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 779,
@@ -3285,9 +3084,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 780,
@@ -3309,9 +3106,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 781,
@@ -3333,9 +3128,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 782,
@@ -3357,9 +3150,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 783,
@@ -3381,9 +3172,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 784,
@@ -3405,9 +3194,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -3516,9 +3303,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 786,
@@ -3540,9 +3325,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 787,
@@ -3564,9 +3347,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 788,
@@ -3588,9 +3369,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 789,
@@ -3612,9 +3391,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 790,
@@ -3636,9 +3413,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 791,
@@ -3660,9 +3435,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 792,
@@ -3684,9 +3457,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -3877,7 +3648,7 @@
"merge_request_diff_id": 14,
"relative_order": 15,
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3889,7 +3660,7 @@
"merge_request_diff_id": 14,
"relative_order": 16,
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3901,7 +3672,7 @@
"merge_request_diff_id": 14,
"relative_order": 17,
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
- "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3913,7 +3684,7 @@
"merge_request_diff_id": 14,
"relative_order": 18,
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
- "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -3925,7 +3696,7 @@
"merge_request_diff_id": 14,
"relative_order": 19,
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
- "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -4016,7 +3787,7 @@
{
"merge_request_diff_id": 14,
"relative_order": 6,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4042,7 +3813,7 @@
{
"merge_request_diff_id": 14,
"relative_order": 8,
- "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -4207,7 +3978,7 @@
},
"events": [
{
- "merge_request_diff_id": 14,
+ "merge_request_diff_id": 14,
"id": 529,
"target_type": "Note",
"target_id": 793,
@@ -4239,9 +4010,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 795,
@@ -4263,9 +4032,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 796,
@@ -4287,9 +4054,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 797,
@@ -4311,9 +4076,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 798,
@@ -4335,9 +4098,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 799,
@@ -4359,9 +4120,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 800,
@@ -4383,9 +4142,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -4603,7 +4360,7 @@
{
"merge_request_diff_id": 13,
"relative_order": 2,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -4740,9 +4497,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 802,
@@ -4764,9 +4519,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 803,
@@ -4788,9 +4541,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 804,
@@ -4812,9 +4563,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 805,
@@ -4836,9 +4585,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 806,
@@ -4860,9 +4607,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 807,
@@ -4884,9 +4629,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 808,
@@ -4908,9 +4651,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -5104,7 +4845,7 @@
{
"merge_request_diff_id": 12,
"relative_order": 2,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -5228,9 +4969,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 810,
@@ -5252,9 +4991,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 811,
@@ -5276,9 +5013,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 812,
@@ -5300,9 +5035,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 813,
@@ -5324,9 +5057,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 814,
@@ -5348,9 +5079,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 815,
@@ -5372,9 +5101,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 816,
@@ -5396,18 +5123,14 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
"id": 11,
"state": "empty",
- "merge_request_diff_commits": [
- ],
- "merge_request_diff_files": [
- ],
+ "merge_request_diff_commits": [],
+ "merge_request_diff_files": [],
"merge_request_id": 11,
"created_at": "2016-06-14T15:02:23.772Z",
"updated_at": "2016-06-14T15:02:23.833Z",
@@ -5482,9 +5205,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 818,
@@ -5506,9 +5227,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 819,
@@ -5530,9 +5249,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 820,
@@ -5554,9 +5271,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 821,
@@ -5578,9 +5293,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 822,
@@ -5602,9 +5315,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 823,
@@ -5626,9 +5337,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 824,
@@ -5650,9 +5359,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -5843,7 +5550,7 @@
"merge_request_diff_id": 10,
"relative_order": 16,
"sha": "5937ac0a7beb003549fc5fd26fc247adbce4a52e",
- "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Add submodule from gitlab.com\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T10:01:38.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5855,7 +5562,7 @@
"merge_request_diff_id": 10,
"relative_order": 17,
"sha": "570e7b2abdd848b95f2f578043fc23bd6f6fd24d",
- "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Change some files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:57:31.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5867,7 +5574,7 @@
"merge_request_diff_id": 10,
"relative_order": 18,
"sha": "6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9",
- "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "More submodules\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:54:21.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5879,7 +5586,7 @@
"merge_request_diff_id": 10,
"relative_order": 19,
"sha": "d14d6c0abdd253381df51a723d58691b2ee1ab08",
- "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Remove ds_store files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:49:50.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5891,7 +5598,7 @@
"merge_request_diff_id": 10,
"relative_order": 20,
"sha": "c1acaa58bbcbc3eafe538cb8274ba387047b69f8",
- "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets \u003cdmitriy.zaporozhets@gmail.com\u003e\n",
+ "message": "Ignore DS files\n\nSigned-off-by: Dmitriy Zaporozhets <dmitriy.zaporozhets@gmail.com>\n",
"authored_date": "2014-02-27T09:48:32.000+01:00",
"author_name": "Dmitriy Zaporozhets",
"author_email": "dmitriy.zaporozhets@gmail.com",
@@ -5982,7 +5689,7 @@
{
"merge_request_diff_id": 10,
"relative_order": 6,
- "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+\u003c?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?\u003e\n+\u003csvg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\"\u003e\n+ \u003c!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch --\u003e\n+ \u003ctitle\u003ewm\u003c/title\u003e\n+ \u003cdesc\u003eCreated with Sketch.\u003c/desc\u003e\n+ \u003cdefs\u003e\n+ \u003cpath id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"\u003e\u003c/path\u003e\n+ \u003c/defs\u003e\n+ \u003cg id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\"\u003e\n+ \u003cpath d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\"\u003e\n+ \u003cg id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\"\u003e\n+ \u003cg id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\n+ \u003cpath d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g16\"\u003e\n+ \u003cg id=\"g18-Clipped\"\u003e\n+ \u003cmask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\"\u003e\n+ \u003cuse xlink:href=\"#path-1\"\u003e\u003c/use\u003e\n+ \u003c/mask\u003e\n+ \u003cg id=\"path22\"\u003e\u003c/g\u003e\n+ \u003cg id=\"g18\" mask=\"url(#mask-2)\"\u003e\n+ \u003cg transform=\"translate(382.736659, 312.879425)\"\u003e\n+ \u003cg id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\"\u003e\n+ \u003cpath d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\"\u003e\n+ \u003cpath d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\"\u003e\n+ \u003cpath d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\"\u003e\n+ \u003cpath d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cpath d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cpath d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003cg id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\"\u003e\n+ \u003cpath d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\"\u003e\n+ \u003cpath d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path54\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\"\u003e\n+ \u003cpath d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cg id=\"path62\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\"\u003e\n+ \u003cpath d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\"\u003e\n+ \u003cg id=\"path70\"\u003e\u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\"\u003e\n+ \u003cpath d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\"\u003e\n+ \u003cpath d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\"\u003e\n+ \u003cpath d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003cg id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\"\u003e\n+ \u003cpath d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"\u003e\u003c/path\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+ \u003c/g\u003e\n+\u003c/svg\u003e\n\\ No newline at end of file\n",
+ "utf8_diff": "--- /dev/null\n+++ b/files/images/wm.svg\n@@ -0,0 +1,78 @@\n+<?xml version=\"1.0\" encoding=\"UTF-8\" standalone=\"no\"?>\n+<svg width=\"1300px\" height=\"680px\" viewBox=\"0 0 1300 680\" version=\"1.1\" xmlns=\"http://www.w3.org/2000/svg\" xmlns:xlink=\"http://www.w3.org/1999/xlink\" xmlns:sketch=\"http://www.bohemiancoding.com/sketch/ns\">\n+ <!-- Generator: Sketch 3.2.2 (9983) - http://www.bohemiancoding.com/sketch -->\n+ <title>wm</title>\n+ <desc>Created with Sketch.</desc>\n+ <defs>\n+ <path id=\"path-1\" d=\"M-69.8,1023.54607 L1675.19996,1023.54607 L1675.19996,0 L-69.8,0 L-69.8,1023.54607 L-69.8,1023.54607 Z\"></path>\n+ </defs>\n+ <g id=\"Page-1\" stroke=\"none\" stroke-width=\"1\" fill=\"none\" fill-rule=\"evenodd\" sketch:type=\"MSPage\">\n+ <path d=\"M1300,680 L0,680 L0,0 L1300,0 L1300,680 L1300,680 Z\" id=\"bg\" fill=\"#30353E\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"gitlab_logo\" sketch:type=\"MSLayerGroup\" transform=\"translate(-262.000000, -172.000000)\">\n+ <g id=\"g10\" transform=\"translate(872.500000, 512.354581) scale(1, -1) translate(-872.500000, -512.354581) translate(0.000000, 0.290751)\">\n+ <g id=\"g12\" transform=\"translate(1218.022652, 440.744871)\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\">\n+ <path d=\"M-50.0233338,141.900706 L-69.07059,141.900706 L-69.0100967,0.155858152 L8.04444805,0.155858152 L8.04444805,17.6840847 L-49.9628405,17.6840847 L-50.0233338,141.900706 L-50.0233338,141.900706 Z\" id=\"path14\"></path>\n+ </g>\n+ <g id=\"g16\">\n+ <g id=\"g18-Clipped\">\n+ <mask id=\"mask-2\" sketch:name=\"path22\" fill=\"white\">\n+ <use xlink:href=\"#path-1\"></use>\n+ </mask>\n+ <g id=\"path22\"></g>\n+ <g id=\"g18\" mask=\"url(#mask-2)\">\n+ <g transform=\"translate(382.736659, 312.879425)\">\n+ <g id=\"g24\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(852.718192, 124.992771)\">\n+ <path d=\"M63.9833317,27.9148929 C59.2218085,22.9379001 51.2134221,17.9597442 40.3909323,17.9597442 C25.8888194,17.9597442 20.0453962,25.1013043 20.0453962,34.4074318 C20.0453962,48.4730484 29.7848226,55.1819277 50.5642821,55.1819277 C54.4602853,55.1819277 60.7364685,54.7492469 63.9833317,54.1002256 L63.9833317,27.9148929 L63.9833317,27.9148929 Z M44.2869356,113.827628 C28.9053426,113.827628 14.7975996,108.376082 3.78897657,99.301416 L10.5211864,87.6422957 C18.3131929,92.1866076 27.8374026,96.7320827 41.4728323,96.7320827 C57.0568452,96.7320827 63.9833317,88.7239978 63.9833317,75.3074024 L63.9833317,68.3821827 C60.9528485,69.0312039 54.6766653,69.4650479 50.7806621,69.4650479 C17.4476729,69.4650479 0.565379986,57.7791759 0.565379986,33.3245665 C0.565379986,11.4683685 13.9844297,0.43151772 34.3299658,0.43151772 C48.0351955,0.43151772 61.1692285,6.70771614 65.7143717,16.8780421 L69.1776149,3.02876588 L82.5978279,3.02876588 L82.5978279,75.5237428 C82.5978279,98.462806 72.6408582,113.827628 44.2869356,113.827628 L44.2869356,113.827628 Z\" id=\"path26\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g28\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(959.546624, 124.857151)\">\n+ <path d=\"M37.2266657,17.4468081 C30.0837992,17.4468081 23.8064527,18.3121698 19.0449295,20.4767371 L19.0449295,79.2306079 L19.0449295,86.0464943 C25.538656,91.457331 33.5470425,95.3526217 43.7203922,95.3526217 C62.1173451,95.3526217 69.2602116,82.3687072 69.2602116,61.3767077 C69.2602116,31.5135879 57.7885819,17.4468081 37.2266657,17.4468081 M45.2315622,113.963713 C28.208506,113.963713 19.0449295,102.384849 19.0449295,102.384849 L19.0449295,120.67143 L18.9844362,144.908535 L10.3967097,144.908535 L0.371103324,144.908535 L0.431596656,6.62629771 C9.73826309,2.73100702 22.5081728,0.567602823 36.3611458,0.567602823 C71.8579349,0.567602823 88.9566078,23.2891625 88.9566078,62.4584098 C88.9566078,93.4043948 73.1527248,113.963713 45.2315622,113.963713\" id=\"path30\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g32\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(509.576747, 125.294950)\">\n+ <path d=\"M68.636665,129.10638 C85.5189579,129.10638 96.3414476,123.480366 103.484314,117.853189 L111.669527,132.029302 C100.513161,141.811145 85.5073245,147.06845 69.5021849,147.06845 C29.0274926,147.06845 0.673569983,122.3975 0.673569983,72.6252464 C0.673569983,20.4709215 31.2622559,0.12910638 66.2553217,0.12910638 C83.7879179,0.12910638 98.7227909,4.24073748 108.462217,8.35236859 L108.063194,64.0763105 L108.063194,70.6502677 L108.063194,81.6057001 L56.1168719,81.6057001 L56.1168719,64.0763105 L89.2323178,64.0763105 L89.6313411,21.7701271 C85.3025779,19.6055598 77.7269514,17.8748364 67.554765,17.8748364 C39.4172223,17.8748364 20.5863462,35.5717154 20.5863462,72.8415868 C20.5863462,110.711628 40.0663623,129.10638 68.636665,129.10638\" id=\"path34\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g36\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(692.388992, 124.376085)\">\n+ <path d=\"M19.7766662,145.390067 L1.16216997,145.390067 L1.2226633,121.585642 L1.2226633,111.846834 L1.2226633,106.170806 L1.2226633,96.2656714 L1.2226633,39.5681976 L1.2226633,39.3518572 C1.2226633,16.4127939 11.1796331,1.04797161 39.5335557,1.04797161 C43.4504989,1.04797161 47.2836822,1.40388649 51.0051854,2.07965952 L51.0051854,18.7925385 C48.3109055,18.3796307 45.4351455,18.1446804 42.3476589,18.1446804 C26.763646,18.1446804 19.8371595,26.1516022 19.8371595,39.5681976 L19.8371595,96.2656714 L51.0051854,96.2656714 L51.0051854,111.846834 L19.8371595,111.846834 L19.7766662,145.390067 L19.7766662,145.390067 Z\" id=\"path38\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <path d=\"M646.318899,128.021188 L664.933395,128.021188 L664.933395,236.223966 L646.318899,236.223966 L646.318899,128.021188 L646.318899,128.021188 Z\" id=\"path40\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <path d=\"M646.318899,251.154944 L664.933395,251.154944 L664.933395,269.766036 L646.318899,269.766036 L646.318899,251.154944 L646.318899,251.154944 Z\" id=\"path42\" fill=\"#8C929D\" sketch:type=\"MSShapeGroup\"></path>\n+ <g id=\"g44\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.464170, 0.676006)\">\n+ <path d=\"M429.269989,169.815599 L405.225053,243.802859 L357.571431,390.440955 C355.120288,397.984955 344.444378,397.984955 341.992071,390.440955 L294.337286,243.802859 L136.094873,243.802859 L88.4389245,390.440955 C85.9877812,397.984955 75.3118715,397.984955 72.8595648,390.440955 L25.2059427,243.802859 L1.16216997,169.815599 C-1.03187664,163.067173 1.37156997,155.674379 7.11261982,151.503429 L215.215498,0.336141836 L423.319539,151.503429 C429.060589,155.674379 431.462873,163.067173 429.269989,169.815599\" id=\"path46\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g48\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(135.410135, 1.012147)\">\n+ <path d=\"M80.269998,0 L80.269998,0 L159.391786,243.466717 L1.14820997,243.466717 L80.269998,0 L80.269998,0 Z\" id=\"path50\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g52\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path54\"></g>\n+ </g>\n+ <g id=\"g56\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(24.893471, 1.012613)\">\n+ <path d=\"M190.786662,0 L111.664874,243.465554 L0.777106647,243.465554 L190.786662,0 L190.786662,0 Z\" id=\"path58\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g60\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <g id=\"path62\"></g>\n+ </g>\n+ <g id=\"g64\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(0.077245, 0.223203)\">\n+ <path d=\"M25.5933327,244.255313 L25.5933327,244.255313 L1.54839663,170.268052 C-0.644486651,163.519627 1.75779662,156.126833 7.50000981,151.957046 L215.602888,0.789758846 L25.5933327,244.255313 L25.5933327,244.255313 Z\" id=\"path66\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g68\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012147)\">\n+ <g id=\"path70\"></g>\n+ </g>\n+ <g id=\"g72\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(25.670578, 244.478283)\">\n+ <path d=\"M0,0 L110.887767,0 L63.2329818,146.638096 C60.7806751,154.183259 50.1047654,154.183259 47.6536221,146.638096 L0,0 L0,0 Z\" id=\"path74\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g76\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(215.680133, 1.012613)\">\n+ <path d=\"M0,0 L79.121788,243.465554 L190.009555,243.465554 L0,0 L0,0 Z\" id=\"path78\" fill=\"#FC6D26\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g80\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(214.902910, 0.223203)\">\n+ <path d=\"M190.786662,244.255313 L190.786662,244.255313 L214.831598,170.268052 C217.024481,163.519627 214.622198,156.126833 208.879985,151.957046 L0.777106647,0.789758846 L190.786662,244.255313 L190.786662,244.255313 Z\" id=\"path82\" fill=\"#FCA326\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ <g id=\"g84\" stroke-width=\"1\" fill=\"none\" sketch:type=\"MSLayerGroup\" transform=\"translate(294.009575, 244.478283)\">\n+ <path d=\"M111.679997,0 L0.79222998,0 L48.4470155,146.638096 C50.8993221,154.183259 61.5752318,154.183259 64.0263751,146.638096 L111.679997,0 L111.679997,0 Z\" id=\"path86\" fill=\"#E24329\" sketch:type=\"MSShapeGroup\"></path>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+ </g>\n+</svg>\n\\ No newline at end of file\n",
"new_path": "files/images/wm.svg",
"old_path": "files/images/wm.svg",
"a_mode": "0",
@@ -6008,7 +5715,7 @@
{
"merge_request_diff_id": 10,
"relative_order": 8,
- "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" =\u003e path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" =\u003e path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output \u003c\u003c stdout.read\n @cmd_output \u003c\u003c stderr.read\n",
+ "utf8_diff": "--- a/files/ruby/popen.rb\n+++ b/files/ruby/popen.rb\n@@ -6,12 +6,18 @@ module Popen\n \n def popen(cmd, path=nil)\n unless cmd.is_a?(Array)\n- raise \"System commands must be given as an array of strings\"\n+ raise RuntimeError, \"System commands must be given as an array of strings\"\n end\n \n path ||= Dir.pwd\n- vars = { \"PWD\" => path }\n- options = { chdir: path }\n+\n+ vars = {\n+ \"PWD\" => path\n+ }\n+\n+ options = {\n+ chdir: path\n+ }\n \n unless File.directory?(path)\n FileUtils.mkdir_p(path)\n@@ -19,6 +25,7 @@ module Popen\n \n @cmd_output = \"\"\n @cmd_status = 0\n+\n Open3.popen3(vars, *cmd, options) do |stdin, stdout, stderr, wait_thr|\n @cmd_output << stdout.read\n @cmd_output << stderr.read\n",
"new_path": "files/ruby/popen.rb",
"old_path": "files/ruby/popen.rb",
"a_mode": "100644",
@@ -6171,9 +5878,7 @@
"author": {
"name": "User 4"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 826,
@@ -6195,9 +5900,7 @@
"author": {
"name": "User 3"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 827,
@@ -6219,9 +5922,7 @@
"author": {
"name": "User 0"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 828,
@@ -6243,9 +5944,7 @@
"author": {
"name": "Ottis Schuster II"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 829,
@@ -6267,9 +5966,7 @@
"author": {
"name": "Rhett Emmerich IV"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 830,
@@ -6291,9 +5988,7 @@
"author": {
"name": "Burdette Bernier"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 831,
@@ -6315,9 +6010,7 @@
"author": {
"name": "Ari Wintheiser"
},
- "events": [
-
- ]
+ "events": []
},
{
"id": 832,
@@ -6339,9 +6032,7 @@
"author": {
"name": "Administrator"
},
- "events": [
-
- ]
+ "events": []
}
],
"merge_request_diff": {
@@ -6953,9 +6644,7 @@
"updated_at": "2017-01-16T15:25:28.637Z"
}
],
- "deploy_keys": [
-
- ],
+ "deploy_keys": [],
"services": [
{
"id": 100,
@@ -6964,9 +6653,7 @@
"created_at": "2016-06-14T15:01:51.315Z",
"updated_at": "2016-06-14T15:01:51.315Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7008,9 +6695,7 @@
"created_at": "2016-06-14T15:01:51.289Z",
"updated_at": "2016-06-14T15:01:51.289Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7030,9 +6715,7 @@
"created_at": "2016-06-14T15:01:51.277Z",
"updated_at": "2016-06-14T15:01:51.277Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7052,9 +6735,7 @@
"created_at": "2016-06-14T15:01:51.267Z",
"updated_at": "2016-06-14T15:01:51.267Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7097,9 +6778,7 @@
"created_at": "2016-06-14T15:01:51.232Z",
"updated_at": "2016-06-14T15:01:51.232Z",
"active": true,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7141,9 +6820,7 @@
"created_at": "2016-06-14T15:01:51.202Z",
"updated_at": "2016-06-14T15:01:51.202Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7163,9 +6840,7 @@
"created_at": "2016-06-14T15:01:51.182Z",
"updated_at": "2016-06-14T15:01:51.182Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7185,9 +6860,7 @@
"created_at": "2016-06-14T15:01:51.166Z",
"updated_at": "2016-06-14T15:01:51.166Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7207,9 +6880,7 @@
"created_at": "2016-06-14T15:01:51.153Z",
"updated_at": "2016-06-14T15:01:51.153Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7229,9 +6900,7 @@
"created_at": "2016-06-14T15:01:51.139Z",
"updated_at": "2016-06-14T15:01:51.139Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7251,9 +6920,7 @@
"created_at": "2016-06-14T15:01:51.125Z",
"updated_at": "2016-06-14T15:01:51.125Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7273,9 +6940,7 @@
"created_at": "2016-06-14T15:01:51.113Z",
"updated_at": "2016-06-14T15:01:51.113Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7295,9 +6960,7 @@
"created_at": "2016-06-14T15:01:51.080Z",
"updated_at": "2016-06-14T15:01:51.080Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7317,9 +6980,7 @@
"created_at": "2016-06-14T15:01:51.067Z",
"updated_at": "2016-06-14T15:01:51.067Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7339,9 +7000,7 @@
"created_at": "2016-06-14T15:01:51.047Z",
"updated_at": "2016-06-14T15:01:51.047Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7361,9 +7020,7 @@
"created_at": "2016-06-14T15:01:51.031Z",
"updated_at": "2016-06-14T15:01:51.031Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7383,9 +7040,7 @@
"created_at": "2016-06-14T15:01:51.031Z",
"updated_at": "2016-06-14T15:01:51.031Z",
"active": false,
- "properties": {
-
- },
+ "properties": {},
"template": false,
"push_events": true,
"issues_events": true,
@@ -7399,9 +7054,7 @@
"type": "JenkinsDeprecatedService"
}
],
- "hooks": [
-
- ],
+ "hooks": [],
"protected_branches": [
{
"id": 1,
@@ -7475,5 +7128,25 @@
"key": "bar",
"value": "bar"
}
+ ],
+ "project_badges": [
+ {
+ "id": 1,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "project_id": 5,
+ "type": "ProjectBadge",
+ "link_url": "http://www.example.com",
+ "image_url": "http://www.example.com"
+ },
+ {
+ "id": 2,
+ "created_at": "2017-10-19T15:36:23.466Z",
+ "updated_at": "2017-10-19T15:36:23.466Z",
+ "project_id": 5,
+ "type": "ProjectBadge",
+ "link_url": "http://www.example.com",
+ "image_url": "http://www.example.com"
+ }
]
}
diff --git a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
index d076007e4bc..1a4d09724fc 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -129,6 +129,10 @@ describe Gitlab::ImportExport::ProjectTreeRestorer do
expect(@project.custom_attributes.count).to eq(2)
end
+ it 'has badges' do
+ expect(@project.project_badges.count).to eq(2)
+ end
+
it 'restores the correct service' do
expect(CustomIssueTrackerService.first).not_to be_nil
end
diff --git a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
index 5804c45871e..d6bd5f5c81d 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -180,6 +180,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
expect(saved_project_json['custom_attributes'].count).to eq(2)
end
+ it 'has badges' do
+ expect(saved_project_json['project_badges'].count).to eq(2)
+ end
+
it 'does not complain about non UTF-8 characters in MR diff files' do
ActiveRecord::Base.connection.execute("UPDATE merge_request_diff_files SET diff = '---\n- :diff: !binary |-\n LS0tIC9kZXYvbnVsbAorKysgYi9pbWFnZXMvbnVjb3IucGRmCkBAIC0wLDAg\n KzEsMTY3OSBAQAorJVBERi0xLjUNJeLjz9MNCisxIDAgb2JqDTw8L01ldGFk\n YXR'")
@@ -288,6 +292,9 @@ describe Gitlab::ImportExport::ProjectTreeSaver do
create(:project_custom_attribute, project: project)
create(:project_custom_attribute, project: project)
+ create(:project_badge, project: project)
+ create(:project_badge, project: project)
+
project
end
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
index f1df44cea75..5c61a5a2044 100644
--- a/spec/lib/gitlab/import_export/relation_factory_spec.rb
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -29,6 +29,7 @@ describe Gitlab::ImportExport::RelationFactory do
'service_id' => service_id,
'push_events' => true,
'issues_events' => false,
+ 'confidential_issues_events' => false,
'merge_requests_events' => true,
'tag_push_events' => false,
'note_events' => true,
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index feaab6673cd..ddcbb7a0033 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -536,3 +536,12 @@ LfsFileLock:
- user_id
- project_id
- created_at
+Badge:
+- id
+- link_url
+- image_url
+- project_id
+- group_id
+- created_at
+- updated_at
+- type
diff --git a/spec/lib/gitlab/kubernetes/config_map_spec.rb b/spec/lib/gitlab/kubernetes/config_map_spec.rb
new file mode 100644
index 00000000000..33dfa461202
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/config_map_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::ConfigMap do
+ let(:kubeclient) { double('kubernetes client') }
+ let(:application) { create(:clusters_applications_prometheus) }
+ let(:config_map) { described_class.new(application.name, application.values) }
+ let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
+
+ let(:metadata) do
+ {
+ name: "values-content-configuration-#{application.name}",
+ namespace: namespace,
+ labels: { name: "values-content-configuration-#{application.name}" }
+ }
+ end
+
+ describe '#generate' do
+ let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) }
+ subject { config_map.generate }
+
+ it 'should build a Kubeclient Resource' do
+ is_expected.to eq(resource)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/api_spec.rb b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
index 69112fe90b1..740466ea5cb 100644
--- a/spec/lib/gitlab/kubernetes/helm/api_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/api_spec.rb
@@ -5,14 +5,21 @@ describe Gitlab::Kubernetes::Helm::Api do
let(:helm) { described_class.new(client) }
let(:gitlab_namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
let(:namespace) { Gitlab::Kubernetes::Namespace.new(gitlab_namespace, client) }
- let(:install_helm) { true }
- let(:chart) { 'stable/a_chart' }
- let(:application_name) { 'app_name' }
- let(:command) { Gitlab::Kubernetes::Helm::InstallCommand.new(application_name, install_helm: install_helm, chart: chart) }
+ let(:application) { create(:clusters_applications_prometheus) }
+
+ let(:command) do
+ Gitlab::Kubernetes::Helm::InstallCommand.new(
+ application.name,
+ chart: application.chart,
+ values: application.values
+ )
+ end
+
subject { helm }
before do
allow(Gitlab::Kubernetes::Namespace).to receive(:new).with(gitlab_namespace, client).and_return(namespace)
+ allow(client).to receive(:create_config_map)
end
describe '#initialize' do
@@ -26,6 +33,7 @@ describe Gitlab::Kubernetes::Helm::Api do
describe '#install' do
before do
allow(client).to receive(:create_pod).and_return(nil)
+ allow(client).to receive(:create_config_map).and_return(nil)
allow(namespace).to receive(:ensure_exists!).once
end
@@ -35,6 +43,16 @@ describe Gitlab::Kubernetes::Helm::Api do
subject.install(command)
end
+
+ context 'with a ConfigMap' do
+ let(:resource) { Gitlab::Kubernetes::ConfigMap.new(application.name, application.values).generate }
+
+ it 'creates a ConfigMap on kubeclient' do
+ expect(client).to receive(:create_config_map).with(resource).once
+
+ subject.install(command)
+ end
+ end
end
describe '#installation_status' do
diff --git a/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
new file mode 100644
index 00000000000..3cfdae794f6
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/base_command_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Helm::BaseCommand do
+ let(:application) { create(:clusters_applications_helm) }
+ let(:base_command) { described_class.new(application.name) }
+
+ describe '#generate_script' do
+ let(:helm_version) { Gitlab::Kubernetes::Helm::HELM_VERSION }
+ let(:command) do
+ <<~HEREDOC
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v#{helm_version}-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ HEREDOC
+ end
+
+ subject { base_command.generate_script }
+
+ it 'should return a command that prepares the environment for helm-cli' do
+ expect(subject).to eq(command)
+ end
+ end
+
+ describe '#pod_resource' do
+ subject { base_command.pod_resource }
+
+ it 'should returns a kubeclient resoure with pod content for application' do
+ is_expected.to be_an_instance_of ::Kubeclient::Resource
+ end
+ end
+
+ describe '#config_map?' do
+ subject { base_command.config_map? }
+
+ it { is_expected.to be_falsy }
+ end
+
+ describe '#pod_name' do
+ subject { base_command.pod_name }
+
+ it { is_expected.to eq('install-helm') }
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
new file mode 100644
index 00000000000..e6920b0a76f
--- /dev/null
+++ b/spec/lib/gitlab/kubernetes/helm/init_command_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe Gitlab::Kubernetes::Helm::InitCommand do
+ let(:application) { create(:clusters_applications_helm) }
+ let(:init_command) { described_class.new(application.name) }
+
+ describe '#generate_script' do
+ let(:command) do
+ <<~MSG.chomp
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ helm init >/dev/null
+ MSG
+ end
+
+ subject { init_command.generate_script }
+
+ it 'should return the appropriate command' do
+ is_expected.to eq(command)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
index 63997a40d52..137b8f718de 100644
--- a/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/install_command_spec.rb
@@ -1,79 +1,56 @@
require 'rails_helper'
describe Gitlab::Kubernetes::Helm::InstallCommand do
- let(:prometheus) { create(:clusters_applications_prometheus) }
-
- describe "#initialize" do
- context "With all the params" do
- subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) }
-
- it 'should assign all parameters' do
- expect(subject.name).to eq(prometheus.name)
- expect(subject.install_helm).to be_truthy
- expect(subject.chart).to eq(prometheus.chart)
- expect(subject.chart_values_file).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
- end
- end
-
- context 'when install_helm is not set' do
- subject { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: true) }
-
- it 'should set install_helm as false' do
- expect(subject.install_helm).to be_falsy
- end
- end
-
- context 'when chart is not set' do
- subject { described_class.new(prometheus.name, install_helm: true) }
+ let(:application) { create(:clusters_applications_prometheus) }
+ let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
+
+ let(:install_command) do
+ described_class.new(
+ application.name,
+ chart: application.chart,
+ values: application.values
+ )
+ end
- it 'should set chart as nil' do
- expect(subject.chart).to be_falsy
- end
+ describe '#generate_script' do
+ let(:command) do
+ <<~MSG
+ set -eo pipefail
+ apk add -U ca-certificates openssl >/dev/null
+ wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
+ mv /tmp/linux-amd64/helm /usr/bin/
+ helm init --client-only >/dev/null
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
+ MSG
end
- context 'when chart_values_file is not set' do
- subject { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart) }
+ subject { install_command.generate_script }
- it 'should set chart_values_file as nil' do
- expect(subject.chart_values_file).to be_falsy
- end
+ it 'should return appropriate command' do
+ is_expected.to eq(command)
end
- end
-
- describe "#generate_script" do
- let(:install_command) { described_class.new(prometheus.name, install_helm: install_helm) }
- let(:client) { double('kubernetes client') }
- let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) }
- subject { install_command.send(:generate_script, namespace.name) }
- context 'when install helm is true' do
- let(:install_helm) { true }
- let(:command) do
- <<~MSG
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
-
- helm init >/dev/null
- MSG
+ context 'with an application with a repository' do
+ let(:ci_runner) { create(:ci_runner) }
+ let(:application) { create(:clusters_applications_runner, runner: ci_runner) }
+ let(:install_command) do
+ described_class.new(
+ application.name,
+ chart: application.chart,
+ values: application.values,
+ repository: application.repository
+ )
end
- it 'should return appropriate command' do
- is_expected.to eq(command)
- end
- end
-
- context 'when install helm is false' do
- let(:install_helm) { false }
let(:command) do
<<~MSG
set -eo pipefail
apk add -U ca-certificates openssl >/dev/null
wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
mv /tmp/linux-amd64/helm /usr/bin/
-
helm init --client-only >/dev/null
+ helm repo add #{application.name} #{application.repository}
+ helm install #{application.chart} --name #{application.name} --namespace #{namespace} -f /data/helm/#{application.name}/config/values.yaml >/dev/null
MSG
end
@@ -81,50 +58,29 @@ describe Gitlab::Kubernetes::Helm::InstallCommand do
is_expected.to eq(command)
end
end
+ end
- context 'when chart is present' do
- let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart) }
- let(:command) do
- <<~MSG.chomp
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
+ describe '#config_map?' do
+ subject { install_command.config_map? }
- helm init --client-only >/dev/null
- helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} >/dev/null
- MSG
- end
+ it { is_expected.to be_truthy }
+ end
- it 'should return appropriate command' do
- is_expected.to eq(command)
- end
+ describe '#config_map_resource' do
+ let(:metadata) do
+ {
+ name: "values-content-configuration-#{application.name}",
+ namespace: namespace,
+ labels: { name: "values-content-configuration-#{application.name}" }
+ }
end
- context 'when chart values file is present' do
- let(:install_command) { described_class.new(prometheus.name, chart: prometheus.chart, chart_values_file: prometheus.chart_values_file) }
- let(:command) do
- <<~MSG.chomp
- set -eo pipefail
- apk add -U ca-certificates openssl >/dev/null
- wget -q -O - https://kubernetes-helm.storage.googleapis.com/helm-v2.7.0-linux-amd64.tar.gz | tar zxC /tmp >/dev/null
- mv /tmp/linux-amd64/helm /usr/bin/
+ let(:resource) { ::Kubeclient::Resource.new(metadata: metadata, data: { values: application.values }) }
- helm init --client-only >/dev/null
- helm install #{prometheus.chart} --name #{prometheus.name} --namespace #{namespace.name} -f /data/helm/#{prometheus.name}/config/values.yaml >/dev/null
- MSG
- end
+ subject { install_command.config_map_resource }
- it 'should return appropriate command' do
- is_expected.to eq(command)
- end
+ it 'returns a KubeClient resource with config map content for the application' do
+ is_expected.to eq(resource)
end
end
-
- describe "#pod_name" do
- let(:install_command) { described_class.new(prometheus.name, install_helm: true, chart: prometheus.chart, chart_values_file: true) }
- subject { install_command.send(:pod_name) }
-
- it { is_expected.to eq('install-prometheus') }
- end
end
diff --git a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
index ebb6033f71e..43adc80d576 100644
--- a/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
+++ b/spec/lib/gitlab/kubernetes/helm/pod_spec.rb
@@ -5,13 +5,9 @@ describe Gitlab::Kubernetes::Helm::Pod do
let(:cluster) { create(:cluster) }
let(:app) { create(:clusters_applications_prometheus, cluster: cluster) }
let(:command) { app.install_command }
- let(:client) { double('kubernetes client') }
- let(:namespace) { Gitlab::Kubernetes::Namespace.new(Gitlab::Kubernetes::Helm::NAMESPACE, client) }
- subject { described_class.new(command, namespace.name, client) }
+ let(:namespace) { Gitlab::Kubernetes::Helm::NAMESPACE }
- before do
- allow(client).to receive(:create_config_map).and_return(nil)
- end
+ subject { described_class.new(command, namespace) }
shared_examples 'helm pod' do
it 'should generate a Kubeclient::Resource' do
@@ -47,7 +43,7 @@ describe Gitlab::Kubernetes::Helm::Pod do
end
end
- context 'with a configuration file' do
+ context 'with a install command' do
it_behaves_like 'helm pod'
it 'should include volumes for the container' do
@@ -62,14 +58,14 @@ describe Gitlab::Kubernetes::Helm::Pod do
end
it 'should mount configMap specification in the volume' do
- spec = subject.generate.spec
- expect(spec.volumes.first.configMap['name']).to eq("values-content-configuration-#{app.name}")
- expect(spec.volumes.first.configMap['items'].first['key']).to eq('values')
- expect(spec.volumes.first.configMap['items'].first['path']).to eq('values.yaml')
+ volume = subject.generate.spec.volumes.first
+ expect(volume.configMap['name']).to eq("values-content-configuration-#{app.name}")
+ expect(volume.configMap['items'].first['key']).to eq('values')
+ expect(volume.configMap['items'].first['path']).to eq('values.yaml')
end
end
- context 'without a configuration file' do
+ context 'with a init command' do
let(:app) { create(:clusters_applications_helm, cluster: cluster) }
it_behaves_like 'helm pod'
diff --git a/spec/lib/gitlab/middleware/read_only_spec.rb b/spec/lib/gitlab/middleware/read_only_spec.rb
index 07ba11b93a3..39ec2f37a83 100644
--- a/spec/lib/gitlab/middleware/read_only_spec.rb
+++ b/spec/lib/gitlab/middleware/read_only_spec.rb
@@ -11,15 +11,17 @@ describe Gitlab::Middleware::ReadOnly do
RSpec::Matchers.define :disallow_request do
match do |middleware|
- flash = middleware.send(:rack_flash)
- flash['alert'] && flash['alert'].include?('You cannot do writing operations')
+ alert = middleware.env['rack.session'].to_hash
+ .dig('flash', 'flashes', 'alert')
+
+ alert&.include?('You cannot perform write operations')
end
end
RSpec::Matchers.define :disallow_request_in_json do
match do |response|
json_response = JSON.parse(response.body)
- response.body.include?('You cannot do writing operations') && json_response.key?('message')
+ response.body.include?('You cannot perform write operations') && json_response.key?('message')
end
end
@@ -34,10 +36,25 @@ describe Gitlab::Middleware::ReadOnly do
rack.to_app
end
- subject { described_class.new(fake_app) }
+ let(:observe_env) do
+ Module.new do
+ attr_reader :env
+
+ def call(env)
+ @env = env
+ super
+ end
+ end
+ end
let(:request) { Rack::MockRequest.new(rack_stack) }
+ subject do
+ described_class.new(fake_app).tap do |app|
+ app.extend(observe_env)
+ end
+ end
+
context 'normal requests to a read-only Gitlab instance' do
let(:fake_app) { lambda { |env| [200, { 'Content-Type' => 'text/plain' }, ['OK']] } }
diff --git a/spec/lib/gitlab/middleware/release_env_spec.rb b/spec/lib/gitlab/middleware/release_env_spec.rb
new file mode 100644
index 00000000000..5e3aa877409
--- /dev/null
+++ b/spec/lib/gitlab/middleware/release_env_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+describe Gitlab::Middleware::ReleaseEnv do
+ let(:inner_app) { double(:app, call: 'yay') }
+ let(:app) { described_class.new(inner_app) }
+ let(:env) { { 'action_controller.instance' => 'something' } }
+
+ describe '#call' do
+ it 'calls the app and clears the env' do
+ result = app.call(env)
+
+ expect(result).to eq('yay')
+ expect(env).to be_empty
+ end
+ end
+end
diff --git a/spec/lib/gitlab/plugin_spec.rb b/spec/lib/gitlab/plugin_spec.rb
new file mode 100644
index 00000000000..33dd4f79130
--- /dev/null
+++ b/spec/lib/gitlab/plugin_spec.rb
@@ -0,0 +1,68 @@
+require 'spec_helper'
+
+describe Gitlab::Plugin do
+ describe '.execute' do
+ let(:data) { Gitlab::DataBuilder::Push::SAMPLE_DATA }
+ let(:plugin) { Rails.root.join('plugins', 'test.rb') }
+ let(:tmp_file) { Tempfile.new('plugin-dump') }
+ let(:result) { described_class.execute(plugin.to_s, data) }
+ let(:success) { result.first }
+ let(:message) { result.last }
+
+ let(:plugin_source) do
+ <<~EOS
+ #!/usr/bin/env ruby
+ x = STDIN.read
+ File.write('#{tmp_file.path}', x)
+ EOS
+ end
+
+ before do
+ File.write(plugin, plugin_source)
+ end
+
+ after do
+ FileUtils.rm(plugin)
+ end
+
+ context 'successful execution' do
+ before do
+ File.chmod(0o777, plugin)
+ end
+
+ after do
+ tmp_file.close!
+ end
+
+ it { expect(success).to be true }
+ it { expect(message).to be_empty }
+
+ it 'ensures plugin received data via stdin' do
+ result
+
+ expect(File.read(tmp_file.path)).to eq(data.to_json)
+ end
+ end
+
+ context 'non-executable' do
+ it { expect(success).to be false }
+ it { expect(message).to include('Permission denied') }
+ end
+
+ context 'non-zero exit' do
+ let(:plugin_source) do
+ <<~EOS
+ #!/usr/bin/env ruby
+ exit 1
+ EOS
+ end
+
+ before do
+ File.chmod(0o777, plugin)
+ end
+
+ it { expect(success).to be false }
+ it { expect(message).to be_empty }
+ end
+ end
+end
diff --git a/spec/lib/gitlab/project_search_results_spec.rb b/spec/lib/gitlab/project_search_results_spec.rb
index 1ebb0105cf5..c46bb8edebf 100644
--- a/spec/lib/gitlab/project_search_results_spec.rb
+++ b/spec/lib/gitlab/project_search_results_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
require 'spec_helper'
describe Gitlab::ProjectSearchResults do
@@ -105,6 +106,32 @@ describe Gitlab::ProjectSearchResults do
end
end
+ context 'when the search returns non-ASCII data' do
+ context 'with UTF-8' do
+ let(:results) { project.repository.search_files_by_content("файл", 'master') }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/russian.rb')
+ expect(subject.basename).to eq('encoding/russian')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("Хороший файл")
+ end
+ end
+
+ context 'with ISO-8859-1' do
+ let(:search_result) { "master:encoding/iso8859.txt\x001\x00\xC4\xFC\nmaster:encoding/iso8859.txt\x002\x00\nmaster:encoding/iso8859.txt\x003\x00foo\n".force_encoding(Encoding::ASCII_8BIT) }
+
+ it 'returns results as UTF-8' do
+ expect(subject.filename).to eq('encoding/iso8859.txt')
+ expect(subject.basename).to eq('encoding/iso8859')
+ expect(subject.ref).to eq('master')
+ expect(subject.startline).to eq(1)
+ expect(subject.data).to eq("Äü\n\nfoo")
+ end
+ end
+ end
+
context "when filename has extension" do
let(:search_result) { "master:CONTRIBUTE.md\x005\x00- [Contribute to GitLab](#contribute-to-gitlab)\n" }
@@ -190,7 +217,7 @@ describe Gitlab::ProjectSearchResults do
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
- expect(results.issues_count).to eq 1
+ expect(results.limited_issues_count).to eq 1
end
it 'does not list project confidential issues for project members with guest role' do
@@ -202,7 +229,7 @@ describe Gitlab::ProjectSearchResults do
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).not_to include security_issue_2
- expect(results.issues_count).to eq 1
+ expect(results.limited_issues_count).to eq 1
end
it 'lists project confidential issues for author' do
@@ -212,7 +239,7 @@ describe Gitlab::ProjectSearchResults do
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).not_to include security_issue_2
- expect(results.issues_count).to eq 2
+ expect(results.limited_issues_count).to eq 2
end
it 'lists project confidential issues for assignee' do
@@ -222,7 +249,7 @@ describe Gitlab::ProjectSearchResults do
expect(issues).to include issue
expect(issues).not_to include security_issue_1
expect(issues).to include security_issue_2
- expect(results.issues_count).to eq 2
+ expect(results.limited_issues_count).to eq 2
end
it 'lists project confidential issues for project members' do
@@ -234,7 +261,7 @@ describe Gitlab::ProjectSearchResults do
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).to include security_issue_2
- expect(results.issues_count).to eq 3
+ expect(results.limited_issues_count).to eq 3
end
it 'lists all project issues for admin' do
@@ -244,7 +271,7 @@ describe Gitlab::ProjectSearchResults do
expect(issues).to include issue
expect(issues).to include security_issue_1
expect(issues).to include security_issue_2
- expect(results.issues_count).to eq 3
+ expect(results.limited_issues_count).to eq 3
end
end
@@ -277,6 +304,35 @@ describe Gitlab::ProjectSearchResults do
end
end
+ describe '#limited_notes_count' do
+ let(:project) { create(:project, :public) }
+ let(:note) { create(:note_on_issue, project: project) }
+ let(:results) { described_class.new(user, project, note.note) }
+
+ context 'when count_limit is lower than total amount' do
+ before do
+ allow(results).to receive(:count_limit).and_return(1)
+ end
+
+ it 'calls note finder once to get the limited amount of notes' do
+ expect(results).to receive(:notes_finder).once.and_call_original
+ expect(results.limited_notes_count).to eq(1)
+ end
+ end
+
+ context 'when count_limit is higher than total amount' do
+ it 'calls note finder multiple times to get the limited amount of notes' do
+ project = create(:project, :public)
+ note = create(:note_on_issue, project: project)
+
+ results = described_class.new(user, project, note.note)
+
+ expect(results).to receive(:notes_finder).exactly(4).times.and_call_original
+ expect(results.limited_notes_count).to eq(1)
+ end
+ end
+ end
+
# Examples for commit access level test
#
# params:
diff --git a/spec/lib/gitlab/search_results_spec.rb b/spec/lib/gitlab/search_results_spec.rb
index 9dbab95f70e..87288baedb0 100644
--- a/spec/lib/gitlab/search_results_spec.rb
+++ b/spec/lib/gitlab/search_results_spec.rb
@@ -29,30 +29,6 @@ describe Gitlab::SearchResults do
end
end
- describe '#projects_count' do
- it 'returns the total amount of projects' do
- expect(results.projects_count).to eq(1)
- end
- end
-
- describe '#issues_count' do
- it 'returns the total amount of issues' do
- expect(results.issues_count).to eq(1)
- end
- end
-
- describe '#merge_requests_count' do
- it 'returns the total amount of merge requests' do
- expect(results.merge_requests_count).to eq(1)
- end
- end
-
- describe '#milestones_count' do
- it 'returns the total amount of milestones' do
- expect(results.milestones_count).to eq(1)
- end
- end
-
context "when count_limit is lower than total amount" do
before do
allow(results).to receive(:count_limit).and_return(1)
@@ -183,7 +159,7 @@ describe Gitlab::SearchResults do
expect(issues).not_to include security_issue_3
expect(issues).not_to include security_issue_4
expect(issues).not_to include security_issue_5
- expect(results.issues_count).to eq 1
+ expect(results.limited_issues_count).to eq 1
end
it 'does not list confidential issues for project members with guest role' do
@@ -199,7 +175,7 @@ describe Gitlab::SearchResults do
expect(issues).not_to include security_issue_3
expect(issues).not_to include security_issue_4
expect(issues).not_to include security_issue_5
- expect(results.issues_count).to eq 1
+ expect(results.limited_issues_count).to eq 1
end
it 'lists confidential issues for author' do
@@ -212,7 +188,7 @@ describe Gitlab::SearchResults do
expect(issues).to include security_issue_3
expect(issues).not_to include security_issue_4
expect(issues).not_to include security_issue_5
- expect(results.issues_count).to eq 3
+ expect(results.limited_issues_count).to eq 3
end
it 'lists confidential issues for assignee' do
@@ -225,7 +201,7 @@ describe Gitlab::SearchResults do
expect(issues).not_to include security_issue_3
expect(issues).to include security_issue_4
expect(issues).not_to include security_issue_5
- expect(results.issues_count).to eq 3
+ expect(results.limited_issues_count).to eq 3
end
it 'lists confidential issues for project members' do
@@ -241,7 +217,7 @@ describe Gitlab::SearchResults do
expect(issues).to include security_issue_3
expect(issues).not_to include security_issue_4
expect(issues).not_to include security_issue_5
- expect(results.issues_count).to eq 4
+ expect(results.limited_issues_count).to eq 4
end
it 'lists all issues for admin' do
@@ -254,7 +230,7 @@ describe Gitlab::SearchResults do
expect(issues).to include security_issue_3
expect(issues).to include security_issue_4
expect(issues).not_to include security_issue_5
- expect(results.issues_count).to eq 5
+ expect(results.limited_issues_count).to eq 5
end
end
diff --git a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb b/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
deleted file mode 100644
index 8fdbbacd04d..00000000000
--- a/spec/lib/gitlab/sidekiq_middleware/memory_killer_spec.rb
+++ /dev/null
@@ -1,63 +0,0 @@
-require 'spec_helper'
-
-describe Gitlab::SidekiqMiddleware::MemoryKiller do
- subject { described_class.new }
- let(:pid) { 999 }
-
- let(:worker) { double(:worker, class: 'TestWorker') }
- let(:job) { { 'jid' => 123 } }
- let(:queue) { 'test_queue' }
-
- def run
- thread = subject.call(worker, job, queue) { nil }
- thread&.join
- end
-
- before do
- allow(subject).to receive(:get_rss).and_return(10.kilobytes)
- allow(subject).to receive(:pid).and_return(pid)
- end
-
- context 'when MAX_RSS is set to 0' do
- before do
- stub_const("#{described_class}::MAX_RSS", 0)
- end
-
- it 'does nothing' do
- expect(subject).not_to receive(:sleep)
-
- run
- end
- end
-
- context 'when MAX_RSS is exceeded' do
- before do
- stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
- end
-
- it 'sends the STP, TERM and KILL signals at expected times' do
- expect(subject).to receive(:sleep).with(15 * 60).ordered
- expect(Process).to receive(:kill).with('SIGSTP', pid).ordered
-
- expect(subject).to receive(:sleep).with(30).ordered
- expect(Process).to receive(:kill).with('SIGTERM', pid).ordered
-
- expect(subject).to receive(:sleep).with(10).ordered
- expect(Process).to receive(:kill).with('SIGKILL', pid).ordered
-
- run
- end
- end
-
- context 'when MAX_RSS is not exceeded' do
- before do
- stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
- end
-
- it 'does nothing' do
- expect(subject).not_to receive(:sleep)
-
- run
- end
- end
-end
diff --git a/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb b/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb
new file mode 100644
index 00000000000..0001795c3f0
--- /dev/null
+++ b/spec/lib/gitlab/sidekiq_middleware/shutdown_spec.rb
@@ -0,0 +1,88 @@
+require 'spec_helper'
+
+describe Gitlab::SidekiqMiddleware::Shutdown do
+ subject { described_class.new }
+
+ let(:pid) { Process.pid }
+ let(:worker) { double(:worker, class: 'TestWorker') }
+ let(:job) { { 'jid' => 123 } }
+ let(:queue) { 'test_queue' }
+ let(:block) { proc { nil } }
+
+ def run
+ subject.call(worker, job, queue) { block.call }
+ described_class.shutdown_thread&.join
+ end
+
+ def pop_trace
+ subject.trace.pop(true)
+ end
+
+ before do
+ allow(subject).to receive(:get_rss).and_return(10.kilobytes)
+ described_class.clear_shutdown_thread
+ end
+
+ context 'when MAX_RSS is set to 0' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 0)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+
+ def expect_shutdown_sequence
+ expect(pop_trace).to eq([:sleep, 15 * 60])
+ expect(pop_trace).to eq([:kill, 'SIGTSTP', pid])
+
+ expect(pop_trace).to eq([:sleep, 30])
+ expect(pop_trace).to eq([:kill, 'SIGTERM', pid])
+
+ expect(pop_trace).to eq([:sleep, 10])
+ expect(pop_trace).to eq([:kill, 'SIGKILL', pid])
+ end
+
+ context 'when MAX_RSS is exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 5.kilobytes)
+ end
+
+ it 'sends the TSTP, TERM and KILL signals at expected times' do
+ run
+
+ expect_shutdown_sequence
+ end
+ end
+
+ context 'when MAX_RSS is not exceeded' do
+ before do
+ stub_const("#{described_class}::MAX_RSS", 15.kilobytes)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:sleep)
+
+ run
+ end
+ end
+
+ context 'when WantShutdown is raised' do
+ let(:block) { proc { raise described_class::WantShutdown } }
+
+ it 'starts the shutdown sequence and re-raises the exception' do
+ expect { run }.to raise_exception(described_class::WantShutdown)
+
+ # We can't expect 'run' to have joined on the shutdown thread, because
+ # it hit an exception.
+ shutdown_thread = described_class.shutdown_thread
+ expect(shutdown_thread).not_to be_nil
+ shutdown_thread.join
+
+ expect_shutdown_sequence
+ end
+ end
+end
diff --git a/spec/lib/gitlab/slash_commands/command_spec.rb b/spec/lib/gitlab/slash_commands/command_spec.rb
index 0173a45d480..e3447d974aa 100644
--- a/spec/lib/gitlab/slash_commands/command_spec.rb
+++ b/spec/lib/gitlab/slash_commands/command_spec.rb
@@ -3,10 +3,11 @@ require 'spec_helper'
describe Gitlab::SlashCommands::Command do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:chat_name) { double(:chat_name, user: user) }
describe '#execute' do
subject do
- described_class.new(project, user, params).execute
+ described_class.new(project, chat_name, params).execute
end
context 'when no command is available' do
@@ -88,7 +89,7 @@ describe Gitlab::SlashCommands::Command do
end
describe '#match_command' do
- subject { described_class.new(project, user, params).match_command.first }
+ subject { described_class.new(project, chat_name, params).match_command.first }
context 'IssueShow is triggered' do
let(:params) { { text: 'issue show 123' } }
diff --git a/spec/lib/gitlab/slash_commands/deploy_spec.rb b/spec/lib/gitlab/slash_commands/deploy_spec.rb
index 74b5ef4bb26..0d57334aa4c 100644
--- a/spec/lib/gitlab/slash_commands/deploy_spec.rb
+++ b/spec/lib/gitlab/slash_commands/deploy_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::SlashCommands::Deploy do
describe '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match('deploy staging to production') }
before do
@@ -16,7 +17,7 @@ describe Gitlab::SlashCommands::Deploy do
end
subject do
- described_class.new(project, user).execute(regex_match)
+ described_class.new(project, chat_name).execute(regex_match)
end
context 'if no environment is defined' do
diff --git a/spec/lib/gitlab/slash_commands/issue_new_spec.rb b/spec/lib/gitlab/slash_commands/issue_new_spec.rb
index 3b077c58c50..8e7df946529 100644
--- a/spec/lib/gitlab/slash_commands/issue_new_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_new_spec.rb
@@ -4,6 +4,7 @@ describe Gitlab::SlashCommands::IssueNew do
describe '#execute' do
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue create bird is the word") }
before do
@@ -11,7 +12,7 @@ describe Gitlab::SlashCommands::IssueNew do
end
subject do
- described_class.new(project, user).execute(regex_match)
+ described_class.new(project, chat_name).execute(regex_match)
end
context 'without description' do
diff --git a/spec/lib/gitlab/slash_commands/issue_search_spec.rb b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
index 35d01efc1bd..189e9592f1b 100644
--- a/spec/lib/gitlab/slash_commands/issue_search_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_search_spec.rb
@@ -6,10 +6,11 @@ describe Gitlab::SlashCommands::IssueSearch do
let!(:confidential) { create(:issue, :confidential, project: project, title: 'mepmep find') }
let(:project) { create(:project) }
let(:user) { create(:user) }
+ let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue search find") }
subject do
- described_class.new(project, user).execute(regex_match)
+ described_class.new(project, chat_name).execute(regex_match)
end
context 'when the user has no access' do
diff --git a/spec/lib/gitlab/slash_commands/issue_show_spec.rb b/spec/lib/gitlab/slash_commands/issue_show_spec.rb
index e5834d5a2ee..b1db1638237 100644
--- a/spec/lib/gitlab/slash_commands/issue_show_spec.rb
+++ b/spec/lib/gitlab/slash_commands/issue_show_spec.rb
@@ -5,6 +5,7 @@ describe Gitlab::SlashCommands::IssueShow do
let(:issue) { create(:issue, project: project) }
let(:project) { create(:project) }
let(:user) { issue.author }
+ let(:chat_name) { double(:chat_name, user: user) }
let(:regex_match) { described_class.match("issue show #{issue.iid}") }
before do
@@ -12,7 +13,7 @@ describe Gitlab::SlashCommands::IssueShow do
end
subject do
- described_class.new(project, user).execute(regex_match)
+ described_class.new(project, chat_name).execute(regex_match)
end
context 'the issue exists' do
diff --git a/spec/lib/gitlab/string_placeholder_replacer_spec.rb b/spec/lib/gitlab/string_placeholder_replacer_spec.rb
new file mode 100644
index 00000000000..7a03ea4154c
--- /dev/null
+++ b/spec/lib/gitlab/string_placeholder_replacer_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+
+describe Gitlab::StringPlaceholderReplacer do
+ describe '.render_url' do
+ it 'returns the nil if the string is blank' do
+ expect(described_class.replace_string_placeholders(nil, /whatever/)).to be_blank
+ end
+
+ it 'returns the string if the placeholder regex' do
+ expect(described_class.replace_string_placeholders('whatever')).to eq 'whatever'
+ end
+
+ it 'returns the string if no block given' do
+ expect(described_class.replace_string_placeholders('whatever', /whatever/)).to eq 'whatever'
+ end
+
+ context 'when all params are valid' do
+ let(:string) { '%{path}/%{id}/%{branch}' }
+ let(:regex) { /(path|id)/ }
+
+ it 'replaces each placeholders with the block result' do
+ result = described_class.replace_string_placeholders(string, regex) do |arg|
+ 'WHATEVER'
+ end
+
+ expect(result).to eq 'WHATEVER/WHATEVER/%{branch}'
+ end
+
+ it 'does not replace the placeholder if the block result is nil' do
+ result = described_class.replace_string_placeholders(string, regex) do |arg|
+ arg == 'path' ? nil : 'WHATEVER'
+ end
+
+ expect(result).to eq '%{path}/WHATEVER/%{branch}'
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/string_regex_marker_spec.rb b/spec/lib/gitlab/string_regex_marker_spec.rb
index d715f9bd641..37b1298b962 100644
--- a/spec/lib/gitlab/string_regex_marker_spec.rb
+++ b/spec/lib/gitlab/string_regex_marker_spec.rb
@@ -2,17 +2,36 @@ require 'spec_helper'
describe Gitlab::StringRegexMarker do
describe '#mark' do
- let(:raw) { %{"name": "AFNetworking"} }
- let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
- subject do
- described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:|
- %{<a href="#">#{text}</a>}
+ context 'with a single occurrence' do
+ let(:raw) { %{"name": "AFNetworking"} }
+ let(:rich) { %{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"AFNetworking"</span>}.html_safe }
+
+ subject do
+ described_class.new(raw, rich).mark(/"[^"]+":\s*"(?<name>[^"]+)"/, group: :name) do |text, left:, right:|
+ %{<a href="#">#{text}</a>}
+ end
+ end
+
+ it 'marks the match' do
+ expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>})
+ expect(subject).to be_html_safe
end
end
- it 'marks the inline diffs' do
- expect(subject).to eq(%{<span class="key">"name"</span><span class="punctuation">: </span><span class="value">"<a href="#">AFNetworking</a>"</span>})
- expect(subject).to be_html_safe
+ context 'with multiple occurrences' do
+ let(:raw) { %{a <b> <c> d} }
+ let(:rich) { %{a &lt;b&gt; &lt;c&gt; d}.html_safe }
+
+ subject do
+ described_class.new(raw, rich).mark(/<[a-z]>/) do |text, left:, right:|
+ %{<strong>#{text}</strong>}
+ end
+ end
+
+ it 'marks the matches' do
+ expect(subject).to eq(%{a <strong>&lt;b&gt;</strong> <strong>&lt;c&gt;</strong> d})
+ expect(subject).to be_html_safe
+ end
end
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index 0e9ecff25a6..138d21ede97 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -36,6 +36,7 @@ describe Gitlab::UsageData do
gitlab_shared_runners
git
database
+ avg_cycle_analytics
))
end
diff --git a/spec/lib/gitlab/verify/lfs_objects_spec.rb b/spec/lib/gitlab/verify/lfs_objects_spec.rb
new file mode 100644
index 00000000000..64f3a9660e0
--- /dev/null
+++ b/spec/lib/gitlab/verify/lfs_objects_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+
+describe Gitlab::Verify::LfsObjects do
+ include GitlabVerifyHelpers
+
+ it_behaves_like 'Gitlab::Verify::BatchVerifier subclass' do
+ let!(:objects) { create_list(:lfs_object, 3, :with_file) }
+ end
+
+ describe '#run_batches' do
+ let(:failures) { collect_failures }
+ let(:failure) { failures[lfs_object] }
+
+ let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) }
+
+ it 'passes LFS objects with the correct file' do
+ expect(failures).to eq({})
+ end
+
+ it 'fails LFS objects with a missing file' do
+ FileUtils.rm_f(lfs_object.file.path)
+
+ expect(failures.keys).to contain_exactly(lfs_object)
+ expect(failure).to be_a(Errno::ENOENT)
+ expect(failure.to_s).to include(lfs_object.file.path)
+ end
+
+ it 'fails LFS objects with a mismatched oid' do
+ File.truncate(lfs_object.file.path, 0)
+
+ expect(failures.keys).to contain_exactly(lfs_object)
+ expect(failure.to_s).to include('Checksum mismatch')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/verify/uploads_spec.rb b/spec/lib/gitlab/verify/uploads_spec.rb
new file mode 100644
index 00000000000..6146ce61226
--- /dev/null
+++ b/spec/lib/gitlab/verify/uploads_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe Gitlab::Verify::Uploads do
+ include GitlabVerifyHelpers
+
+ it_behaves_like 'Gitlab::Verify::BatchVerifier subclass' do
+ let(:projects) { create_list(:project, 3, :with_avatar) }
+ let!(:objects) { projects.flat_map(&:uploads) }
+ end
+
+ describe '#run_batches' do
+ let(:project) { create(:project, :with_avatar) }
+ let(:failures) { collect_failures }
+ let(:failure) { failures[upload] }
+
+ let!(:upload) { project.uploads.first }
+
+ it 'passes uploads with the correct file' do
+ expect(failures).to eq({})
+ end
+
+ it 'fails uploads with a missing file' do
+ FileUtils.rm_f(upload.absolute_path)
+
+ expect(failures.keys).to contain_exactly(upload)
+ expect(failure).to be_a(Errno::ENOENT)
+ expect(failure.to_s).to include(upload.absolute_path)
+ end
+
+ it 'fails uploads with a mismatched checksum' do
+ upload.update!(checksum: 'something incorrect')
+
+ expect(failures.keys).to contain_exactly(upload)
+ expect(failure.to_s).to include('Checksum mismatch')
+ end
+
+ it 'fails uploads with a missing precalculated checksum' do
+ upload.update!(checksum: '')
+
+ expect(failures.keys).to contain_exactly(upload)
+ expect(failure.to_s).to include('Checksum missing')
+ end
+ end
+end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index bcbb9287199..83c33797bbc 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -457,7 +457,7 @@ describe Notify do
it 'has the correct subject and body' do
is_expected.to have_subject("#{project.name} | Project was moved")
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text(project.ssh_url_to_repo)
end
end
@@ -483,8 +483,8 @@ describe Notify do
to_emails = subject.header[:to].addrs.map(&:address)
expect(to_emails).to eq([recipient.notification_email])
- is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Request to join the #{project.full_name} project"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project_project_members_url(project)
is_expected.to have_body_text project_member.human_access
end
@@ -503,8 +503,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
- is_expected.to have_subject "Access to the #{project.name_with_namespace} project was denied"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Access to the #{project.full_name} project was denied"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
end
end
@@ -520,8 +520,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
- is_expected.to have_subject "Access to the #{project.name_with_namespace} project was granted"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Access to the #{project.full_name} project was granted"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
end
@@ -550,8 +550,8 @@ describe Notify do
it_behaves_like "a user cannot unsubscribe through footer link"
it 'contains all the useful information' do
- is_expected.to have_subject "Invitation to join the #{project.name_with_namespace} project"
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_subject "Invitation to join the #{project.full_name} project"
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.human_access
is_expected.to have_body_text project_member.invite_token
@@ -575,7 +575,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation accepted'
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
is_expected.to have_html_escaped_body_text invited_user.name
@@ -598,7 +598,7 @@ describe Notify do
it 'contains all the useful information' do
is_expected.to have_subject 'Invitation declined'
- is_expected.to have_html_escaped_body_text project.name_with_namespace
+ is_expected.to have_html_escaped_body_text project.full_name
is_expected.to have_body_text project.web_url
is_expected.to have_body_text project_member.invite_email
end
diff --git a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
index b47f3314926..033d0e7584d 100644
--- a/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
+++ b/spec/migrations/cleanup_namespaceless_pending_delete_projects_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170502101023_cleanup_namespaceless_pending_delete_projects.rb')
-describe CleanupNamespacelessPendingDeleteProjects do
+describe CleanupNamespacelessPendingDeleteProjects, :migration, schema: 20180222043024 do
before do
# Stub after_save callbacks that will fail when Project has no namespace
allow_any_instance_of(Project).to receive(:ensure_storage_path_exists).and_return(nil)
diff --git a/spec/migrations/migrate_issues_to_ghost_user_spec.rb b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
index ff0d44e1ed2..9220b49a736 100644
--- a/spec/migrations/migrate_issues_to_ghost_user_spec.rb
+++ b/spec/migrations/migrate_issues_to_ghost_user_spec.rb
@@ -8,7 +8,7 @@ describe MigrateIssuesToGhostUser, :migration do
let(:users) { table(:users) }
before do
- project = projects.create!(name: 'gitlab')
+ project = projects.create!(name: 'gitlab', namespace_id: 1)
user = users.create(email: 'test@example.com')
issues.create(title: 'Issue 1', author_id: nil, project_id: project.id)
issues.create(title: 'Issue 2', author_id: user.id, project_id: project.id)
diff --git a/spec/migrations/migrate_stages_statuses_spec.rb b/spec/migrations/migrate_stages_statuses_spec.rb
index 79d2708f9ad..ce35276cbf5 100644
--- a/spec/migrations/migrate_stages_statuses_spec.rb
+++ b/spec/migrations/migrate_stages_statuses_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170711145558_migrate_stages_statuses.rb')
-describe MigrateStagesStatuses, :migration do
+describe MigrateStagesStatuses, :sidekiq, :migration do
let(:jobs) { table(:ci_builds) }
let(:stages) { table(:ci_stages) }
let(:pipelines) { table(:ci_pipelines) }
diff --git a/spec/migrations/schedule_build_stage_migration_spec.rb b/spec/migrations/schedule_build_stage_migration_spec.rb
new file mode 100644
index 00000000000..e2ca35447fb
--- /dev/null
+++ b/spec/migrations/schedule_build_stage_migration_spec.rb
@@ -0,0 +1,35 @@
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180212101928_schedule_build_stage_migration')
+
+describe ScheduleBuildStageMigration, :sidekiq, :migration do
+ let(:projects) { table(:projects) }
+ let(:pipelines) { table(:ci_pipelines) }
+ let(:stages) { table(:ci_stages) }
+ let(:jobs) { table(:ci_builds) }
+
+ before do
+ stub_const("#{described_class}::BATCH_SIZE", 1)
+
+ projects.create!(id: 123, name: 'gitlab', path: 'gitlab-ce')
+ pipelines.create!(id: 1, project_id: 123, ref: 'master', sha: 'adf43c3a')
+ stages.create!(id: 1, project_id: 123, pipeline_id: 1, name: 'test')
+
+ jobs.create!(id: 11, commit_id: 1, project_id: 123, stage_id: nil)
+ jobs.create!(id: 206, commit_id: 1, project_id: 123, stage_id: nil)
+ jobs.create!(id: 3413, commit_id: 1, project_id: 123, stage_id: nil)
+ jobs.create!(id: 4109, commit_id: 1, project_id: 123, stage_id: 1)
+ end
+
+ it 'schedules delayed background migrations in batches in bulk' do
+ Sidekiq::Testing.fake! do
+ Timecop.freeze do
+ migrate!
+
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(5.minutes, 11, 11)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(10.minutes, 206, 206)
+ expect(described_class::MIGRATION).to be_scheduled_delayed_migration(15.minutes, 3413, 3413)
+ expect(BackgroundMigrationWorker.jobs.size).to eq 3
+ end
+ end
+ end
+end
diff --git a/spec/models/badge_spec.rb b/spec/models/badge_spec.rb
new file mode 100644
index 00000000000..33dc19e3432
--- /dev/null
+++ b/spec/models/badge_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+describe Badge do
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+
+ describe 'validations' do
+ # Requires the let variable url_sym
+ shared_examples 'placeholder url' do
+ let(:badge) { build(:badge) }
+
+ it 'allows url with http protocol' do
+ badge[url_sym] = 'http://www.example.com'
+
+ expect(badge).to be_valid
+ end
+
+ it 'allows url with https protocol' do
+ badge[url_sym] = 'https://www.example.com'
+
+ expect(badge).to be_valid
+ end
+
+ it 'cannot be empty' do
+ badge[url_sym] = ''
+
+ expect(badge).not_to be_valid
+ end
+
+ it 'cannot be nil' do
+ badge[url_sym] = nil
+
+ expect(badge).not_to be_valid
+ end
+
+ it 'accept badges placeholders' do
+ badge[url_sym] = placeholder_url
+
+ expect(badge).to be_valid
+ end
+
+ it 'sanitize url' do
+ badge[url_sym] = 'javascript:alert(1)'
+
+ expect(badge).not_to be_valid
+ end
+ end
+
+ context 'link_url format' do
+ let(:url_sym) { :link_url }
+
+ it_behaves_like 'placeholder url'
+ end
+
+ context 'image_url format' do
+ let(:url_sym) { :image_url }
+
+ it_behaves_like 'placeholder url'
+ end
+ end
+
+ shared_examples 'rendered_links' do
+ it 'should use the project information to populate the url placeholders' do
+ stub_project_commit_info(project)
+
+ expect(badge.public_send("rendered_#{method}", project)).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
+ end
+
+ it 'returns the url if the project used is nil' do
+ expect(badge.public_send("rendered_#{method}", nil)).to eq placeholder_url
+ end
+
+ def stub_project_commit_info(project)
+ allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever'))
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+ end
+
+ context 'methods' do
+ let(:badge) { build(:badge, link_url: placeholder_url, image_url: placeholder_url) }
+ let!(:project) { create(:project) }
+
+ context '#rendered_link_url' do
+ let(:method) { :link_url }
+
+ it_behaves_like 'rendered_links'
+ end
+
+ context '#rendered_image_url' do
+ let(:method) { :image_url }
+
+ it_behaves_like 'rendered_links'
+ end
+ end
+end
diff --git a/spec/models/badges/group_badge_spec.rb b/spec/models/badges/group_badge_spec.rb
new file mode 100644
index 00000000000..ed7f83d0489
--- /dev/null
+++ b/spec/models/badges/group_badge_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe GroupBadge do
+ describe 'associations' do
+ it { is_expected.to belong_to(:group) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:group) }
+ end
+end
diff --git a/spec/models/badges/project_badge_spec.rb b/spec/models/badges/project_badge_spec.rb
new file mode 100644
index 00000000000..0e1a8159cb6
--- /dev/null
+++ b/spec/models/badges/project_badge_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+
+describe ProjectBadge do
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+
+ describe 'associations' do
+ it { is_expected.to belong_to(:project) }
+ end
+
+ describe 'validations' do
+ it { is_expected.to validate_presence_of(:project) }
+ end
+
+ shared_examples 'rendered_links' do
+ it 'should use the badge project information to populate the url placeholders' do
+ stub_project_commit_info(project)
+
+ expect(badge.public_send("rendered_#{method}")).to eq "http://www.example.com/#{project.full_path}/#{project.id}/master/whatever"
+ end
+
+ def stub_project_commit_info(project)
+ allow(project).to receive(:commit).and_return(double('Commit', sha: 'whatever'))
+ allow(project).to receive(:default_branch).and_return('master')
+ end
+ end
+
+ context 'methods' do
+ let(:badge) { build(:project_badge, link_url: placeholder_url, image_url: placeholder_url) }
+ let!(:project) { badge.project }
+
+ context '#rendered_link_url' do
+ let(:method) { :link_url }
+
+ it_behaves_like 'rendered_links'
+ end
+
+ context '#rendered_image_url' do
+ let(:method) { :image_url }
+
+ it_behaves_like 'rendered_links'
+ end
+ end
+end
diff --git a/spec/models/chat_name_spec.rb b/spec/models/chat_name_spec.rb
index e89e534d914..504bc710b25 100644
--- a/spec/models/chat_name_spec.rb
+++ b/spec/models/chat_name_spec.rb
@@ -14,4 +14,24 @@ describe ChatName do
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to(:service_id) }
it { is_expected.to validate_uniqueness_of(:chat_id).scoped_to(:service_id, :team_id) }
+
+ describe '#update_last_used_at', :clean_gitlab_redis_shared_state do
+ it 'updates the last_used_at timestamp' do
+ expect(subject.last_used_at).to be_nil
+
+ subject.update_last_used_at
+
+ expect(subject.last_used_at).to be_present
+ end
+
+ it 'does not update last_used_at if it was recently updated' do
+ subject.update_last_used_at
+
+ time = subject.last_used_at
+
+ subject.update_last_used_at
+
+ expect(subject.last_used_at).to eq(time)
+ end
+ end
end
diff --git a/spec/models/ci/group_variable_spec.rb b/spec/models/ci/group_variable_spec.rb
index 145189e7469..1b10501701c 100644
--- a/spec/models/ci/group_variable_spec.rb
+++ b/spec/models/ci/group_variable_spec.rb
@@ -5,7 +5,7 @@ describe Ci::GroupVariable do
it { is_expected.to include_module(HasVariable) }
it { is_expected.to include_module(Presentable) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:group_id).with_message(/\(\w+\) has already been taken/) }
describe '.unprotected' do
subject { described_class.unprotected }
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index e4ff551151e..875e8b2b682 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -6,7 +6,7 @@ describe Ci::Variable do
describe 'validations' do
it { is_expected.to include_module(HasVariable) }
it { is_expected.to include_module(Presentable) }
- it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope) }
+ it { is_expected.to validate_uniqueness_of(:key).scoped_to(:project_id, :environment_scope).with_message(/\(\w+\) has already been taken/) }
end
describe '.unprotected' do
diff --git a/spec/models/clusters/applications/helm_spec.rb b/spec/models/clusters/applications/helm_spec.rb
index eb57abaf6ef..ba7bad617b4 100644
--- a/spec/models/clusters/applications/helm_spec.rb
+++ b/spec/models/clusters/applications/helm_spec.rb
@@ -1,102 +1,17 @@
require 'rails_helper'
describe Clusters::Applications::Helm do
- it { is_expected.to belong_to(:cluster) }
- it { is_expected.to validate_presence_of(:cluster) }
-
- describe '#name' do
- it 'is .application_name' do
- expect(subject.name).to eq(described_class.application_name)
- end
-
- it 'is recorded in Clusters::Cluster::APPLICATIONS' do
- expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
- end
- end
-
- describe '#version' do
- it 'defaults to Gitlab::Kubernetes::Helm::HELM_VERSION' do
- expect(subject.version).to eq(Gitlab::Kubernetes::Helm::HELM_VERSION)
- end
- end
-
- describe '#status' do
- let(:cluster) { create(:cluster) }
-
- subject { described_class.new(cluster: cluster) }
-
- it 'defaults to :not_installable' do
- expect(subject.status_name).to be(:not_installable)
- end
-
- context 'when platform kubernetes is defined' do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
-
- it 'defaults to :installable' do
- expect(subject.status_name).to be(:installable)
- end
- end
- end
+ include_examples 'cluster application core specs', :clusters_applications_helm
describe '#install_command' do
- it 'has all the needed information' do
- expect(subject.install_command).to have_attributes(name: subject.name, install_helm: true)
- end
- end
-
- describe 'status state machine' do
- describe '#make_installing' do
- subject { create(:clusters_applications_helm, :scheduled) }
-
- it 'is installing' do
- subject.make_installing!
-
- expect(subject).to be_installing
- end
- end
-
- describe '#make_installed' do
- subject { create(:clusters_applications_helm, :installing) }
-
- it 'is installed' do
- subject.make_installed
-
- expect(subject).to be_installed
- end
- end
-
- describe '#make_errored' do
- subject { create(:clusters_applications_helm, :installing) }
- let(:reason) { 'some errors' }
-
- it 'is errored' do
- subject.make_errored(reason)
-
- expect(subject).to be_errored
- expect(subject.status_reason).to eq(reason)
- end
- end
-
- describe '#make_scheduled' do
- subject { create(:clusters_applications_helm, :installable) }
-
- it 'is scheduled' do
- subject.make_scheduled
-
- expect(subject).to be_scheduled
- end
-
- describe 'when was errored' do
- subject { create(:clusters_applications_helm, :errored) }
+ let(:helm) { create(:clusters_applications_helm) }
- it 'clears #status_reason' do
- expect(subject.status_reason).not_to be_nil
+ subject { helm.install_command }
- subject.make_scheduled!
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InitCommand) }
- expect(subject.status_reason).to be_nil
- end
- end
+ it 'should be initialized with 1 arguments' do
+ expect(subject.name).to eq('helm')
end
end
end
diff --git a/spec/models/clusters/applications/ingress_spec.rb b/spec/models/clusters/applications/ingress_spec.rb
index 619c088b0bf..03f5b88a525 100644
--- a/spec/models/clusters/applications/ingress_spec.rb
+++ b/spec/models/clusters/applications/ingress_spec.rb
@@ -1,8 +1,78 @@
require 'rails_helper'
describe Clusters::Applications::Ingress do
- it { is_expected.to belong_to(:cluster) }
- it { is_expected.to validate_presence_of(:cluster) }
+ let(:ingress) { create(:clusters_applications_ingress) }
- include_examples 'cluster application specs', described_class
+ include_examples 'cluster application core specs', :clusters_applications_ingress
+ include_examples 'cluster application status specs', :cluster_application_ingress
+
+ before do
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_in)
+ allow(ClusterWaitForIngressIpAddressWorker).to receive(:perform_async)
+ end
+
+ describe '#make_installed!' do
+ before do
+ application.make_installed!
+ end
+
+ let(:application) { create(:clusters_applications_ingress, :installing) }
+
+ it 'schedules a ClusterWaitForIngressIpAddressWorker' do
+ expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_in)
+ .with(Clusters::Applications::Ingress::FETCH_IP_ADDRESS_DELAY, 'ingress', application.id)
+ end
+ end
+
+ describe '#schedule_status_update' do
+ let(:application) { create(:clusters_applications_ingress, :installed) }
+
+ before do
+ application.schedule_status_update
+ end
+
+ it 'schedules a ClusterWaitForIngressIpAddressWorker' do
+ expect(ClusterWaitForIngressIpAddressWorker).to have_received(:perform_async)
+ .with('ingress', application.id)
+ end
+
+ context 'when the application is not installed' do
+ let(:application) { create(:clusters_applications_ingress, :installing) }
+
+ it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
+ expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_async)
+ end
+ end
+
+ context 'when there is already an external_ip' do
+ let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '111.222.222.111') }
+
+ it 'does not schedule a ClusterWaitForIngressIpAddressWorker' do
+ expect(ClusterWaitForIngressIpAddressWorker).not_to have_received(:perform_in)
+ end
+ end
+ end
+
+ describe '#install_command' do
+ subject { ingress.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'should be initialized with ingress arguments' do
+ expect(subject.name).to eq('ingress')
+ expect(subject.chart).to eq('stable/nginx-ingress')
+ expect(subject.values).to eq(ingress.values)
+ end
+ end
+
+ describe '#values' do
+ subject { ingress.values }
+
+ it 'should include ingress valid keys' do
+ is_expected.to include('image')
+ is_expected.to include('repository')
+ is_expected.to include('stats')
+ is_expected.to include('podAnnotations')
+ end
+ end
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 959fb338672..2905b58066b 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -1,10 +1,8 @@
require 'rails_helper'
describe Clusters::Applications::Prometheus do
- it { is_expected.to belong_to(:cluster) }
- it { is_expected.to validate_presence_of(:cluster) }
-
- include_examples 'cluster application specs', described_class
+ include_examples 'cluster application core specs', :clusters_applications_prometheus
+ include_examples 'cluster application status specs', :cluster_application_prometheus
describe 'transition to installed' do
let(:project) { create(:project) }
@@ -24,14 +22,6 @@ describe Clusters::Applications::Prometheus do
end
end
- describe "#chart_values_file" do
- subject { create(:clusters_applications_prometheus).chart_values_file }
-
- it 'should return chart values file path' do
- expect(subject).to eq("#{Rails.root}/vendor/prometheus/values.yaml")
- end
- end
-
describe '#prometheus_client' do
context 'cluster is nil' do
it 'returns nil' do
@@ -85,4 +75,33 @@ describe Clusters::Applications::Prometheus do
end
end
end
+
+ describe '#install_command' do
+ let(:kubeclient) { double('kubernetes client') }
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'should be initialized with 3 arguments' do
+ expect(subject.name).to eq('prometheus')
+ expect(subject.chart).to eq('stable/prometheus')
+ expect(subject.values).to eq(prometheus.values)
+ end
+ end
+
+ describe '#values' do
+ let(:prometheus) { create(:clusters_applications_prometheus) }
+
+ subject { prometheus.values }
+
+ it 'should include prometheus valid values' do
+ is_expected.to include('alertmanager')
+ is_expected.to include('kubeStateMetrics')
+ is_expected.to include('nodeExporter')
+ is_expected.to include('pushgateway')
+ is_expected.to include('serverFiles')
+ end
+ end
end
diff --git a/spec/models/clusters/applications/runner_spec.rb b/spec/models/clusters/applications/runner_spec.rb
new file mode 100644
index 00000000000..a574779e39d
--- /dev/null
+++ b/spec/models/clusters/applications/runner_spec.rb
@@ -0,0 +1,99 @@
+require 'rails_helper'
+
+describe Clusters::Applications::Runner do
+ let(:ci_runner) { create(:ci_runner) }
+
+ include_examples 'cluster application core specs', :clusters_applications_runner
+ include_examples 'cluster application status specs', :cluster_application_runner
+
+ it { is_expected.to belong_to(:runner) }
+
+ describe '#install_command' do
+ let(:kubeclient) { double('kubernetes client') }
+ let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
+
+ subject { gitlab_runner.install_command }
+
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
+
+ it 'should be initialized with 4 arguments' do
+ expect(subject.name).to eq('runner')
+ expect(subject.chart).to eq('runner/gitlab-runner')
+ expect(subject.repository).to eq('https://charts.gitlab.io')
+ expect(subject.values).to eq(gitlab_runner.values)
+ end
+ end
+
+ describe '#values' do
+ let(:gitlab_runner) { create(:clusters_applications_runner, runner: ci_runner) }
+
+ subject { gitlab_runner.values }
+
+ it 'should include runner valid values' do
+ is_expected.to include('concurrent')
+ is_expected.to include('checkInterval')
+ is_expected.to include('rbac')
+ is_expected.to include('runners')
+ is_expected.to include('privileged: true')
+ is_expected.to include('image: ubuntu:16.04')
+ is_expected.to include('resources')
+ is_expected.to include("runnerToken: #{ci_runner.token}")
+ is_expected.to include("gitlabUrl: #{Gitlab::Routing.url_helpers.root_url}")
+ end
+
+ context 'without a runner' do
+ let(:project) { create(:project) }
+ let(:cluster) { create(:cluster) }
+ let(:gitlab_runner) { create(:clusters_applications_runner, cluster: cluster) }
+
+ before do
+ cluster.projects << project
+ end
+
+ it 'creates a runner' do
+ expect do
+ subject
+ end.to change { Ci::Runner.count }.by(1)
+ end
+
+ it 'uses the new runner token' do
+ expect(subject).to include("runnerToken: #{gitlab_runner.reload.runner.token}")
+ end
+
+ it 'assigns the new runner to runner' do
+ subject
+ gitlab_runner.reload
+
+ expect(gitlab_runner.runner).not_to be_nil
+ end
+ end
+
+ context 'with duplicated values on vendor/runner/values.yaml' do
+ let(:values) do
+ {
+ "concurrent" => 4,
+ "checkInterval" => 3,
+ "rbac" => {
+ "create" => false
+ },
+ "clusterWideAccess" => false,
+ "runners" => {
+ "privileged" => false,
+ "image" => "ubuntu:16.04",
+ "builds" => {},
+ "services" => {},
+ "helpers" => {}
+ }
+ }
+ end
+
+ before do
+ allow(gitlab_runner).to receive(:chart_values).and_return(values)
+ end
+
+ it 'should overwrite values.yaml' do
+ is_expected.to include("privileged: #{gitlab_runner.privileged}")
+ end
+ end
+ end
+end
diff --git a/spec/models/clusters/cluster_spec.rb b/spec/models/clusters/cluster_spec.rb
index 799d7ced116..8f12a0e3085 100644
--- a/spec/models/clusters/cluster_spec.rb
+++ b/spec/models/clusters/cluster_spec.rb
@@ -8,6 +8,7 @@ describe Clusters::Cluster do
it { is_expected.to have_one(:application_helm) }
it { is_expected.to have_one(:application_ingress) }
it { is_expected.to have_one(:application_prometheus) }
+ it { is_expected.to have_one(:application_runner) }
it { is_expected.to delegate_method(:status).to(:provider) }
it { is_expected.to delegate_method(:status_reason).to(:provider) }
it { is_expected.to delegate_method(:status_name).to(:provider) }
@@ -196,9 +197,10 @@ describe Clusters::Cluster do
let!(:helm) { create(:clusters_applications_helm, cluster: cluster) }
let!(:ingress) { create(:clusters_applications_ingress, cluster: cluster) }
let!(:prometheus) { create(:clusters_applications_prometheus, cluster: cluster) }
+ let!(:runner) { create(:clusters_applications_runner, cluster: cluster) }
it 'returns a list of created applications' do
- is_expected.to contain_exactly(helm, ingress, prometheus)
+ is_expected.to contain_exactly(helm, ingress, prometheus, runner)
end
end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index c536dab2681..b7ed8be69fc 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -368,7 +368,9 @@ describe CommitStatus do
'rspec:windows 0 : / 1' => 'rspec:windows',
'rspec:windows 0 : / 1 name' => 'rspec:windows name',
'0 1 name ruby' => 'name ruby',
- '0 :/ 1 name ruby' => 'name ruby'
+ '0 :/ 1 name ruby' => 'name ruby',
+ 'golang test 1.8' => 'golang test',
+ '1.9 golang test' => 'golang test'
}
tests.each do |name, group_name|
diff --git a/spec/models/cycle_analytics/code_spec.rb b/spec/models/cycle_analytics/code_spec.rb
index f2f1928926c..6a6b58fb52b 100644
--- a/spec/models/cycle_analytics/code_spec.rb
+++ b/spec/models/cycle_analytics/code_spec.rb
@@ -18,11 +18,11 @@ describe 'CycleAnalytics#code' do
end]],
end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
+ context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: -> (context, data) do
- context.merge_merge_requests_closing_issue(data[:issue])
- context.deploy_master
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
+ context.deploy_master(context.user, context.project)
end)
context "when a regular merge request (that doesn't close the issue) is created" do
@@ -30,10 +30,10 @@ describe 'CycleAnalytics#code' do
issue = create(:issue, project: project)
create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_merge_request_closing_issue(user, project, issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
- deploy_master
+ merge_merge_requests_closing_issue(user, project, issue)
+ deploy_master(user, project)
expect(subject[:code].median).to be_nil
end
@@ -50,10 +50,10 @@ describe 'CycleAnalytics#code' do
end]],
end_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
+ context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: -> (context, data) do
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when a regular merge request (that doesn't close the issue) is created" do
@@ -61,9 +61,9 @@ describe 'CycleAnalytics#code' do
issue = create(:issue, project: project)
create_commit_referencing_issue(issue)
- create_merge_request_closing_issue(issue, message: "Closes nothing")
+ create_merge_request_closing_issue(user, project, issue, message: "Closes nothing")
- merge_merge_requests_closing_issue(issue)
+ merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:code].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/issue_spec.rb b/spec/models/cycle_analytics/issue_spec.rb
index 985e1bf80be..45f1b4fe8a3 100644
--- a/spec/models/cycle_analytics/issue_spec.rb
+++ b/spec/models/cycle_analytics/issue_spec.rb
@@ -26,8 +26,8 @@ describe 'CycleAnalytics#issue' do
end]],
post_fn: -> (context, data) do
if data[:issue].persisted?
- context.create_merge_request_closing_issue(data[:issue].reload)
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.create_merge_request_closing_issue(context.user, context.project, data[:issue].reload)
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end
end)
@@ -37,8 +37,8 @@ describe 'CycleAnalytics#issue' do
issue = create(:issue, project: project)
issue.update(label_ids: [regular_label.id])
- create_merge_request_closing_issue(issue)
- merge_merge_requests_closing_issue(issue)
+ create_merge_request_closing_issue(user, project, issue)
+ merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/plan_spec.rb b/spec/models/cycle_analytics/plan_spec.rb
index 6fbb2a2d102..d366e2b723a 100644
--- a/spec/models/cycle_analytics/plan_spec.rb
+++ b/spec/models/cycle_analytics/plan_spec.rb
@@ -29,8 +29,8 @@ describe 'CycleAnalytics#plan' do
context.create_commit_referencing_issue(data[:issue], branch_name: data[:branch_name])
end]],
post_fn: -> (context, data) do
- context.create_merge_request_closing_issue(data[:issue], source_branch: data[:branch_name])
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.create_merge_request_closing_issue(context.user, context.project, data[:issue], source_branch: data[:branch_name])
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when a regular label (instead of a list label) is added to the issue" do
@@ -41,8 +41,8 @@ describe 'CycleAnalytics#plan' do
issue.update(label_ids: [label.id])
create_commit_referencing_issue(issue, branch_name: branch_name)
- create_merge_request_closing_issue(issue, source_branch: branch_name)
- merge_merge_requests_closing_issue(issue)
+ create_merge_request_closing_issue(user, project, issue, source_branch: branch_name)
+ merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:issue].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/production_spec.rb b/spec/models/cycle_analytics/production_spec.rb
index f8681c0a2f9..156eb96cfce 100644
--- a/spec/models/cycle_analytics/production_spec.rb
+++ b/spec/models/cycle_analytics/production_spec.rb
@@ -13,11 +13,11 @@ describe 'CycleAnalytics#production' do
data_fn: -> (context) { { issue: context.build(:issue, project: context.project) } },
start_time_conditions: [["issue is created", -> (context, data) { data[:issue].save }]],
before_end_fn: lambda do |context, data|
- context.create_merge_request_closing_issue(data[:issue])
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end,
end_time_conditions:
- [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master }],
+ [["merge request that closes issue is deployed to production", -> (context, data) { context.deploy_master(context.user, context.project) }],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
# Make other changes on master
@@ -29,14 +29,14 @@ describe 'CycleAnalytics#production' do
branch_name: 'master')
context.project.repository.commit(sha)
- context.deploy_master
+ context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
+ deploy_master(user, project)
expect(subject[:production].median).to be_nil
end
@@ -45,9 +45,9 @@ describe 'CycleAnalytics#production' do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
+ merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
+ deploy_master(user, project, environment: 'staging')
expect(subject[:production].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/review_spec.rb b/spec/models/cycle_analytics/review_spec.rb
index 0ac58695b35..0aedfb49cb5 100644
--- a/spec/models/cycle_analytics/review_spec.rb
+++ b/spec/models/cycle_analytics/review_spec.rb
@@ -13,11 +13,11 @@ describe 'CycleAnalytics#review' do
data_fn: -> (context) { { issue: context.create(:issue, project: context.project) } },
start_time_conditions: [["merge request that closes issue is created",
-> (context, data) do
- context.create_merge_request_closing_issue(data[:issue])
+ context.create_merge_request_closing_issue(context.user, context.project, data[:issue])
end]],
end_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end]],
post_fn: nil)
diff --git a/spec/models/cycle_analytics/staging_spec.rb b/spec/models/cycle_analytics/staging_spec.rb
index b66d5623910..0cbda50c688 100644
--- a/spec/models/cycle_analytics/staging_spec.rb
+++ b/spec/models/cycle_analytics/staging_spec.rb
@@ -13,15 +13,15 @@ describe 'CycleAnalytics#staging' do
phase: :staging,
data_fn: lambda do |context|
issue = context.create(:issue, project: context.project)
- { issue: issue, merge_request: context.create_merge_request_closing_issue(issue) }
+ { issue: issue, merge_request: context.create_merge_request_closing_issue(context.user, context.project, issue) }
end,
start_time_conditions: [["merge request that closes issue is merged",
-> (context, data) do
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end]],
end_time_conditions: [["merge request that closes issue is deployed to production",
-> (context, data) do
- context.deploy_master
+ context.deploy_master(context.user, context.project)
end],
["production deploy happens after merge request is merged (along with other changes)",
lambda do |context, data|
@@ -34,14 +34,14 @@ describe 'CycleAnalytics#staging' do
branch_name: 'master')
context.project.repository.commit(sha)
- context.deploy_master
+ context.deploy_master(context.user, context.project)
end]])
context "when a regular merge request (that doesn't close the issue) is merged and deployed" do
it "returns nil" do
merge_request = create(:merge_request)
MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master
+ deploy_master(user, project)
expect(subject[:staging].median).to be_nil
end
@@ -50,9 +50,9 @@ describe 'CycleAnalytics#staging' do
context "when the deployment happens to a non-production environment" do
it "returns nil" do
issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
+ merge_request = create_merge_request_closing_issue(user, project, issue)
MergeRequests::MergeService.new(project, user).execute(merge_request)
- deploy_master(environment: 'staging')
+ deploy_master(user, project, environment: 'staging')
expect(subject[:staging].median).to be_nil
end
diff --git a/spec/models/cycle_analytics/test_spec.rb b/spec/models/cycle_analytics/test_spec.rb
index 690c09bc2dc..e58b8fdff58 100644
--- a/spec/models/cycle_analytics/test_spec.rb
+++ b/spec/models/cycle_analytics/test_spec.rb
@@ -12,26 +12,26 @@ describe 'CycleAnalytics#test' do
phase: :test,
data_fn: lambda do |context|
issue = context.create(:issue, project: context.project)
- merge_request = context.create_merge_request_closing_issue(issue)
+ merge_request = context.create_merge_request_closing_issue(context.user, context.project, issue)
pipeline = context.create(:ci_pipeline, ref: merge_request.source_branch, sha: merge_request.diff_head_sha, project: context.project, head_pipeline_of: merge_request)
{ pipeline: pipeline, issue: issue }
end,
start_time_conditions: [["pipeline is started", -> (context, data) { data[:pipeline].run! }]],
end_time_conditions: [["pipeline is finished", -> (context, data) { data[:pipeline].succeed! }]],
post_fn: -> (context, data) do
- context.merge_merge_requests_closing_issue(data[:issue])
+ context.merge_merge_requests_closing_issue(context.user, context.project, data[:issue])
end)
context "when the pipeline is for a regular merge request (that doesn't close an issue)" do
it "returns nil" do
issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
+ merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run!
pipeline.succeed!
- merge_merge_requests_closing_issue(issue)
+ merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil
end
@@ -51,13 +51,13 @@ describe 'CycleAnalytics#test' do
context "when the pipeline is dropped (failed)" do
it "returns nil" do
issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
+ merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run!
pipeline.drop!
- merge_merge_requests_closing_issue(issue)
+ merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil
end
@@ -66,13 +66,13 @@ describe 'CycleAnalytics#test' do
context "when the pipeline is cancelled" do
it "returns nil" do
issue = create(:issue, project: project)
- merge_request = create_merge_request_closing_issue(issue)
+ merge_request = create_merge_request_closing_issue(user, project, issue)
pipeline = create(:ci_pipeline, ref: "refs/heads/#{merge_request.source_branch}", sha: merge_request.diff_head_sha)
pipeline.run!
pipeline.cancel!
- merge_merge_requests_closing_issue(issue)
+ merge_merge_requests_closing_issue(user, project, issue)
expect(subject[:test].median).to be_nil
end
diff --git a/spec/models/cycle_analytics_spec.rb b/spec/models/cycle_analytics_spec.rb
new file mode 100644
index 00000000000..0fe24870f02
--- /dev/null
+++ b/spec/models/cycle_analytics_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe CycleAnalytics do
+ let(:project) { create(:project, :repository) }
+ let(:from_date) { 10.days.ago }
+ let(:user) { create(:user, :admin) }
+ let(:issue) { create(:issue, project: project, created_at: 2.days.ago) }
+ let(:milestone) { create(:milestone, project: project) }
+ let(:mr) { create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}") }
+ let(:pipeline) { create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr) }
+
+ subject { described_class.new(project, from: from_date) }
+
+ describe '#all_medians_per_stage' do
+ before do
+ allow_any_instance_of(Gitlab::ReferenceExtractor).to receive(:issues).and_return([issue])
+
+ create_cycle(user, project, issue, mr, milestone, pipeline)
+ deploy_master(user, project)
+ end
+
+ it 'returns every median for each stage for a specific project' do
+ values = described_class::STAGES.each_with_object({}) do |stage_name, hsh|
+ hsh[stage_name] = subject[stage_name].median.presence
+ end
+
+ expect(subject.all_medians_per_stage).to eq(values)
+ end
+ end
+end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 4f16b73ef38..abfc0896a41 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -18,6 +18,7 @@ describe Group do
it { is_expected.to have_many(:uploads).dependent(:destroy) }
it { is_expected.to have_one(:chat_team) }
it { is_expected.to have_many(:custom_attributes).class_name('GroupCustomAttribute') }
+ it { is_expected.to have_many(:badges).class_name('GroupBadge') }
describe '#members & #requesters' do
let(:requester) { create(:user) }
diff --git a/spec/models/project_services/asana_service_spec.rb b/spec/models/project_services/asana_service_spec.rb
index 04440d890aa..e66109fd98f 100644
--- a/spec/models/project_services/asana_service_spec.rb
+++ b/spec/models/project_services/asana_service_spec.rb
@@ -47,7 +47,7 @@ describe AsanaService do
it 'calls Asana service to create a story' do
data = create_data_for_commits('Message from commit. related to #123456')
- expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.name_with_namespace} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
+ expected_message = "#{data[:user_name]} pushed to branch #{data[:ref]} of #{project.full_name} ( #{data[:commits][0][:url]} ): #{data[:commits][0][:message]}"
d1 = double('Asana::Task')
expect(d1).to receive(:add_comment).with(text: expected_message)
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index 23db29cb541..3e2a166cdd6 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -29,7 +29,7 @@ describe HipchatService do
let(:user) { create(:user) }
let(:project) { create(:project, :repository) }
let(:api_url) { 'https://hipchat.example.com/v2/room/123456/notification?auth_token=verySecret' }
- let(:project_name) { project.name_with_namespace.gsub(/\s/, '') }
+ let(:project_name) { project.full_name.gsub(/\s/, '') }
let(:token) { 'verySecret' }
let(:server_url) { 'https://hipchat.example.com'}
let(:push_sample_data) do
@@ -303,7 +303,7 @@ describe HipchatService do
message = hipchat.__send__(:create_pipeline_message, data)
project_url = project.web_url
- project_name = project.name_with_namespace.gsub(/\s/, '')
+ project_name = project.full_name.gsub(/\s/, '')
pipeline_attributes = data[:object_attributes]
ref = pipeline_attributes[:ref]
ref_type = pipeline_attributes[:tag] ? 'tag' : 'branch'
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 748c366efca..54ef0be67ff 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -166,7 +166,6 @@ describe JiraService do
# Creates comment
expect(WebMock).to have_requested(:post, @comment_url)
-
# Creates Remote Link in JIRA issue fields
expect(WebMock).to have_requested(:post, @remote_link_url).with(
body: hash_including(
@@ -174,7 +173,7 @@ describe JiraService do
object: {
url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}",
title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: true }
}
)
diff --git a/spec/models/project_services/mattermost_slash_commands_service_spec.rb b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
index 522cf15f3ba..a5bdf9a9337 100644
--- a/spec/models/project_services/mattermost_slash_commands_service_spec.rb
+++ b/spec/models/project_services/mattermost_slash_commands_service_spec.rb
@@ -31,10 +31,10 @@ describe MattermostSlashCommandsService do
url: 'http://trigger.url',
icon_url: 'http://icon.url/icon.png',
auto_complete: true,
- auto_complete_desc: "Perform common operations on: #{project.name_with_namespace}",
+ auto_complete_desc: "Perform common operations on: #{project.full_name}",
auto_complete_hint: '[help]',
- description: "Perform common operations on: #{project.name_with_namespace}",
- display_name: "GitLab / #{project.name_with_namespace}",
+ description: "Perform common operations on: #{project.full_name}",
+ display_name: "GitLab / #{project.full_name}",
method: 'P',
username: 'GitLab'
}.to_json)
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 56c2d7b953e..92ea8841123 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -80,6 +80,7 @@ describe Project do
it { is_expected.to have_many(:members_and_requesters) }
it { is_expected.to have_many(:clusters) }
it { is_expected.to have_many(:custom_attributes).class_name('ProjectCustomAttribute') }
+ it { is_expected.to have_many(:project_badges).class_name('ProjectBadge') }
it { is_expected.to have_many(:lfs_file_locks) }
context 'after initialized' do
@@ -2499,7 +2500,8 @@ describe Project do
end
it 'is a no-op when there is no namespace' do
- project.update_column(:namespace_id, nil)
+ project.namespace.delete
+ project.reload
expect_any_instance_of(Projects::UpdatePagesConfigurationService).not_to receive(:execute)
expect_any_instance_of(Gitlab::PagesTransfer).not_to receive(:rename_project)
@@ -2531,7 +2533,8 @@ describe Project do
it 'is a no-op on legacy projects when there is no namespace' do
export_path = legacy_project.export_path
- legacy_project.update_column(:namespace_id, nil)
+ legacy_project.namespace.delete
+ legacy_project.reload
expect(FileUtils).not_to receive(:rm_rf).with(export_path)
@@ -2543,7 +2546,8 @@ describe Project do
it 'runs on hashed storage projects when there is no namespace' do
export_path = project.export_path
- project.update_column(:namespace_id, nil)
+ project.namespace.delete
+ legacy_project.reload
allow(FileUtils).to receive(:rm_rf).and_call_original
expect(FileUtils).to receive(:rm_rf).with(export_path).and_call_original
@@ -3328,4 +3332,36 @@ describe Project do
end.not_to raise_error # Sidekiq::Worker::EnqueueFromTransactionError
end
end
+
+ describe '#badges' do
+ let(:project_group) { create(:group) }
+ let(:project) { create(:project, path: 'avatar', namespace: project_group) }
+
+ before do
+ create_list(:project_badge, 2, project: project)
+ create(:group_badge, group: project_group)
+ end
+
+ it 'returns the project and the project group badges' do
+ create(:group_badge, group: create(:group))
+
+ expect(Badge.count).to eq 4
+ expect(project.badges.count).to eq 3
+ end
+
+ if Group.supports_nested_groups?
+ context 'with nested_groups' do
+ let(:parent_group) { create(:group) }
+
+ before do
+ create_list(:group_badge, 2, group: project_group)
+ project_group.update(parent: parent_group)
+ end
+
+ it 'returns the project and the project nested groups badges' do
+ expect(project.badges.count).to eq 5
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 1e7671476f1..8b4b5873704 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -14,13 +14,13 @@ describe ProjectWiki do
it { is_expected.to delegate_method(:repository_storage_path).to :project }
it { is_expected.to delegate_method(:hashed_storage?).to :project }
- describe "#path_with_namespace" do
+ describe "#full_path" do
it "returns the project path with namespace with the .wiki extension" do
- expect(subject.path_with_namespace).to eq(project.full_path + '.wiki')
+ expect(subject.full_path).to eq(project.full_path + '.wiki')
end
it 'returns the same value as #full_path' do
- expect(subject.path_with_namespace).to eq(subject.full_path)
+ expect(subject.full_path).to eq(subject.full_path)
end
end
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 0bc07dc7a85..38653e18306 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -242,23 +242,51 @@ describe Repository do
end
describe '#commits' do
- it 'sets follow when path is a single path' do
- expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
-
- repository.commits('master', limit: 1, path: 'README.md')
- repository.commits('master', limit: 1, path: ['README.md'])
+ context 'when neither the all flag nor a ref are specified' do
+ it 'returns every commit from default branch' do
+ expect(repository.commits(limit: 60).size).to eq(37)
+ end
end
- it 'does not set follow when path is multiple paths' do
- expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+ context 'when ref is passed' do
+ it 'returns every commit from the specified ref' do
+ expect(repository.commits('master', limit: 60).size).to eq(37)
+ end
- repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG'])
- end
+ context 'when all' do
+ it 'returns every commit from the repository' do
+ expect(repository.commits('master', limit: 60, all: true).size).to eq(60)
+ end
+ end
+
+ context 'with path' do
+ it 'sets follow when it is a single path' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: true)).and_call_original.twice
+
+ repository.commits('master', limit: 1, path: 'README.md')
+ repository.commits('master', limit: 1, path: ['README.md'])
+ end
- it 'does not set follow when there are no paths' do
- expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+ it 'does not set follow when it is multiple paths' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
- repository.commits('master', limit: 1)
+ repository.commits('master', limit: 1, path: ['README.md', 'CHANGELOG'])
+ end
+ end
+
+ context 'without path' do
+ it 'does not set follow' do
+ expect(Gitlab::Git::Commit).to receive(:where).with(a_hash_including(follow: false)).and_call_original
+
+ repository.commits('master', limit: 1)
+ end
+ end
+ end
+
+ context "when 'all' flag is set" do
+ it 'returns every commit from the repository' do
+ expect(repository.commits(all: true, limit: 60).size).to eq(60)
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 3531de244bd..00b5226d874 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -1635,6 +1635,32 @@ describe User do
end
end
+ describe '#authorizations_for_projects' do
+ let!(:user) { create(:user) }
+ subject { Project.where("EXISTS (?)", user.authorizations_for_projects) }
+
+ it 'includes projects that belong to a user, but no other projects' do
+ owned = create(:project, :private, namespace: user.namespace)
+ member = create(:project, :private).tap { |p| p.add_master(user) }
+ other = create(:project)
+
+ expect(subject).to include(owned)
+ expect(subject).to include(member)
+ expect(subject).not_to include(other)
+ end
+
+ it 'includes projects a user has access to, but no other projects' do
+ other_user = create(:user)
+ accessible = create(:project, :private, namespace: other_user.namespace) do |project|
+ project.add_developer(user)
+ end
+ other = create(:project)
+
+ expect(subject).to include(accessible)
+ expect(subject).not_to include(other)
+ end
+ end
+
describe '#authorized_projects', :delete do
context 'with a minimum access level' do
it 'includes projects for which the user is an owner' do
diff --git a/spec/requests/api/badges_spec.rb b/spec/requests/api/badges_spec.rb
new file mode 100644
index 00000000000..ae64a9ca162
--- /dev/null
+++ b/spec/requests/api/badges_spec.rb
@@ -0,0 +1,367 @@
+require 'spec_helper'
+
+describe API::Badges do
+ let(:master) { create(:user, username: 'master_user') }
+ let(:developer) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:stranger) { create(:user) }
+ let(:project_group) { create(:group) }
+ let(:project) { setup_project }
+ let!(:group) { setup_group }
+
+ shared_context 'source helpers' do
+ def get_source(source_type)
+ source_type == 'project' ? project : group
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/badges' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges", stranger) }
+ end
+
+ %i[master developer access_requester stranger].each do |type|
+ context "when authenticated as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+ badges_count = source_type == 'project' ? 3 : 2
+
+ get api("/#{source_type.pluralize}/#{source.id}/badges", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(badges_count)
+ end
+ end
+ end
+
+ it 'avoids N+1 queries' do
+ # Establish baseline
+ get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+
+ control = ActiveRecord::QueryRecorder.new do
+ get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ end
+
+ project.add_developer(create(:user))
+
+ expect do
+ get api("/#{source_type.pluralize}/#{source.id}/badges", master)
+ end.not_to exceed_query_limit(control)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/badges/:badge_id' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { get api("/#{source_type.pluralize}/#{source.id}/badges/#{developer.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member' do
+ %i[master developer access_requester stranger].each do |type|
+ let(:badge) { source.badges.first }
+
+ context "as a #{type}" do
+ it 'returns 200' do
+ user = public_send(type)
+
+ get api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['id']).to eq(badge.id)
+ expect(json_response['link_url']).to eq(badge.link_url)
+ expect(json_response['rendered_link_url']).to eq(badge.rendered_link_url)
+ expect(json_response['image_url']).to eq(badge.image_url)
+ expect(json_response['rendered_image_url']).to eq(badge.rendered_image_url)
+ expect(json_response['kind']).to eq source_type
+ end
+ end
+ end
+ end
+ end
+ end
+
+ shared_examples 'POST /:sources/:id/badges' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+ let(:example_url) { 'http://www.example.com' }
+ let(:example_url2) { 'http://www.example1.com' }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", stranger),
+ link_url: example_url, image_url: example_url2
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ post api("/#{source_type.pluralize}/#{source.id}/badges", user),
+ link_url: example_url, image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'creates a new badge' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ link_url: example_url, image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(201)
+ end.to change { source.badges.count }.by(1)
+
+ expect(json_response['link_url']).to eq(example_url)
+ expect(json_response['image_url']).to eq(example_url2)
+ expect(json_response['kind']).to eq source_type
+ end
+ end
+
+ it 'returns 400 when link_url is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ link_url: example_url
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when image_url is not given' do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when link_url or image_url is not valid' do
+ post api("/#{source_type.pluralize}/#{source.id}/badges", master),
+ link_url: 'whatever', image_url: 'whatever'
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ shared_examples 'PUT /:sources/:id/badges/:badge_id' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ let(:badge) { source.badges.first }
+ let(:example_url) { 'http://www.example.com' }
+ let(:example_url2) { 'http://www.example1.com' }
+
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger),
+ link_url: example_url
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user),
+ link_url: example_url
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'updates the member' do
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master),
+ link_url: example_url, image_url: example_url2
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response['link_url']).to eq(example_url)
+ expect(json_response['image_url']).to eq(example_url2)
+ expect(json_response['kind']).to eq source_type
+ end
+ end
+
+ it 'returns 400 when link_url or image_url is not valid' do
+ put api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master),
+ link_url: 'whatever', image_url: 'whatever'
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ shared_examples 'DELETE /:sources/:id/badges/:badge_id' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+
+ context "with :sources == #{source_type.pluralize}" do
+ let(:badge) { source.badges.first }
+
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) { delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", stranger) }
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester developer stranger].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'deletes the badge' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master)
+
+ expect(response).to have_gitlab_http_status(204)
+ end.to change { source.badges.count }.by(-1)
+ end
+
+ it_behaves_like '412 response' do
+ let(:request) { api("/#{source_type.pluralize}/#{source.id}/badges/#{badge.id}", master) }
+ end
+ end
+
+ it 'returns 404 if badge does not exist' do
+ delete api("/#{source_type.pluralize}/#{source.id}/badges/123", master)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ shared_examples 'GET /:sources/:id/badges/render' do |source_type|
+ include_context 'source helpers'
+
+ let(:source) { get_source(source_type) }
+ let(:example_url) { 'http://www.example.com' }
+ let(:example_url2) { 'http://www.example1.com' }
+
+ context "with :sources == #{source_type.pluralize}" do
+ it_behaves_like 'a 404 response when source is private' do
+ let(:route) do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", stranger)
+ end
+ end
+
+ context 'when authenticated as a non-member or member with insufficient rights' do
+ %i[access_requester stranger developer].each do |type|
+ context "as a #{type}" do
+ it 'returns 403' do
+ user = public_send(type)
+
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+ end
+
+ context 'when authenticated as a master/owner' do
+ it 'gets the rendered badge values' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}&image_url=#{example_url2}", master)
+
+ expect(response).to have_gitlab_http_status(200)
+
+ expect(json_response.keys).to contain_exactly('link_url', 'rendered_link_url', 'image_url', 'rendered_image_url')
+ expect(json_response['link_url']).to eq(example_url)
+ expect(json_response['image_url']).to eq(example_url2)
+ expect(json_response['rendered_link_url']).to eq(example_url)
+ expect(json_response['rendered_image_url']).to eq(example_url2)
+ end
+ end
+
+ it 'returns 400 when link_url is not given' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=#{example_url}", master)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when image_url is not given' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?image_url=#{example_url}", master)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+
+ it 'returns 400 when link_url or image_url is not valid' do
+ get api("/#{source_type.pluralize}/#{source.id}/badges/render?link_url=whatever&image_url=whatever", master)
+
+ expect(response).to have_gitlab_http_status(400)
+ end
+ end
+ end
+
+ context 'when deleting a badge' do
+ context 'and the source is a project' do
+ it 'cannot delete badges owned by the project group' do
+ delete api("/projects/#{project.id}/badges/#{project_group.badges.first.id}", master)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
+ end
+
+ describe 'Endpoints' do
+ %w(project group).each do |source_type|
+ it_behaves_like 'GET /:sources/:id/badges', source_type
+ it_behaves_like 'GET /:sources/:id/badges/:badge_id', source_type
+ it_behaves_like 'GET /:sources/:id/badges/render', source_type
+ it_behaves_like 'POST /:sources/:id/badges', source_type
+ it_behaves_like 'PUT /:sources/:id/badges/:badge_id', source_type
+ it_behaves_like 'DELETE /:sources/:id/badges/:badge_id', source_type
+ end
+ end
+
+ def setup_project
+ create(:project, :public, :access_requestable, creator_id: master.id, namespace: project_group) do |project|
+ project.add_developer(developer)
+ project.add_master(master)
+ project.request_access(access_requester)
+ project.project_badges << build(:project_badge, project: project)
+ project.project_badges << build(:project_badge, project: project)
+ project_group.badges << build(:group_badge, group: group)
+ end
+ end
+
+ def setup_group
+ create(:group, :public, :access_requestable) do |group|
+ group.add_developer(developer)
+ group.add_owner(master)
+ group.request_access(access_requester)
+ group.badges << build(:group_badge, group: group)
+ group.badges << build(:group_badge, group: group)
+ end
+ end
+end
diff --git a/spec/requests/api/branches_spec.rb b/spec/requests/api/branches_spec.rb
index e433597f58b..64f51d9843d 100644
--- a/spec/requests/api/branches_spec.rb
+++ b/spec/requests/api/branches_spec.rb
@@ -39,6 +39,27 @@ describe API::Branches do
end
end
+ context 'when search parameter is passed' do
+ context 'and branch exists' do
+ it 'returns correct branches' do
+ get api(route, user), per_page: 100, search: branch_name
+
+ searched_branch_names = json_response.map { |branch| branch['name'] }
+ project_branch_names = project.repository.branch_names.grep(/#{branch_name}/)
+
+ expect(searched_branch_names).to match_array(project_branch_names)
+ end
+ end
+
+ context 'and branch does not exist' do
+ it 'returns an empty array' do
+ get api(route, user), per_page: 100, search: 'no_such_branch_name_entropy_of_jabadabadu'
+
+ expect(json_response).to eq []
+ end
+ end
+ end
+
context 'when unauthenticated', 'and project is public' do
before do
project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index ad3eec88952..852f67db958 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -149,6 +149,18 @@ describe API::Commits do
end
end
+ context 'all optional parameter' do
+ it 'returns all project commits' do
+ commit_count = project.repository.count_commits(all: true)
+
+ get api("/projects/#{project_id}/repository/commits?all=true", user)
+
+ expect(response).to include_pagination_headers
+ expect(response.headers['X-Total']).to eq(commit_count.to_s)
+ expect(response.headers['X-Page']).to eql('1')
+ end
+ end
+
context 'with pagination params' do
let(:page) { 1 }
let(:per_page) { 5 }
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index e6d7b9fde02..6614e8cea43 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -163,6 +163,42 @@ describe API::Issues do
expect(first_issue['id']).to eq(issue.id)
end
+ context 'filtering before a specific date' do
+ let!(:issue2) { create(:issue, project: project, author: user, created_at: Date.new(2000, 1, 1), updated_at: Date.new(2000, 1, 1)) }
+
+ it 'returns issues created before a specific date' do
+ get api('/issues?created_before=2000-01-02T00:00:00.060Z', user)
+
+ expect(json_response.size).to eq(1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+
+ it 'returns issues updated before a specific date' do
+ get api('/issues?updated_before=2000-01-02T00:00:00.060Z', user)
+
+ expect(json_response.size).to eq(1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+ end
+
+ context 'filtering after a specific date' do
+ let!(:issue2) { create(:issue, project: project, author: user, created_at: 1.week.from_now, updated_at: 1.week.from_now) }
+
+ it 'returns issues created after a specific date' do
+ get api("/issues?created_after=#{issue2.created_at}", user)
+
+ expect(json_response.size).to eq(1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+
+ it 'returns issues updated after a specific date' do
+ get api("/issues?updated_after=#{issue2.updated_at}", user)
+
+ expect(json_response.size).to eq(1)
+ expect(first_issue['id']).to eq(issue2.id)
+ end
+ end
+
it 'returns an array of labeled issues' do
get api("/issues", user), labels: label.title
@@ -1417,7 +1453,7 @@ describe API::Issues do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
- post api("/projects/12345/issues/#{issue.iid}/move", user),
+ post api("/projects/0/issues/#{issue.iid}/move", user),
to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404)
@@ -1428,7 +1464,7 @@ describe API::Issues do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post api("/projects/#{project.id}/issues/#{issue.iid}/move", user),
- to_project_id: 12345
+ to_project_id: 0
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 14dd9da119d..484322752c0 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -9,6 +9,7 @@ describe API::MergeRequests do
let(:non_member) { create(:user) }
let!(:project) { create(:project, :public, :repository, creator: user, namespace: user.namespace, only_allow_merge_if_pipeline_succeeds: false) }
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
+ let(:pipeline) { create(:ci_empty_pipeline) }
let(:milestone1) { create(:milestone, title: '0.9', project: project) }
let!(:merge_request) { create(:merge_request, :simple, milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Test", created_at: base_time) }
let!(:merge_request_closed) { create(:merge_request, state: "closed", milestone: milestone1, author: user, assignee: user, source_project: project, target_project: project, title: "Closed test", created_at: base_time + 1.second) }
@@ -151,6 +152,62 @@ describe API::MergeRequests do
expect(json_response.first['id']).to eq(merge_request3.id)
end
+ context 'source_branch param' do
+ it 'returns merge requests with the given source branch' do
+ get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |mr| mr['id'] })
+ .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ end
+ end
+
+ context 'target_branch param' do
+ it 'returns merge requests with the given target branch' do
+ get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |mr| mr['id'] })
+ .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ end
+ end
+
+ it 'returns merge requests created before a specific date' do
+ merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', created_at: Date.new(2000, 1, 1))
+
+ get api('/merge_requests?created_before=2000-01-02T00:00:00.060Z', user)
+
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request2.id)
+ end
+
+ it 'returns merge requests created after a specific date' do
+ merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', created_at: 1.week.from_now)
+
+ get api("/merge_requests?created_after=#{merge_request2.created_at}", user)
+
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request2.id)
+ end
+
+ it 'returns merge requests updated before a specific date' do
+ merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', updated_at: Date.new(2000, 1, 1))
+
+ get api('/merge_requests?updated_before=2000-01-02T00:00:00.060Z', user)
+
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request2.id)
+ end
+
+ it 'returns merge requests updated after a specific date' do
+ merge_request2 = create(:merge_request, :simple, source_project: project, target_project: project, source_branch: 'feature_1', updated_at: 1.week.from_now)
+
+ get api("/merge_requests?updated_after=#{merge_request2.updated_at}", user)
+
+ expect(json_response.size).to eq(1)
+ expect(json_response.first['id']).to eq(merge_request2.id)
+ end
+
context 'search params' do
before do
merge_request.update(title: 'Search title', description: 'Search description')
@@ -426,6 +483,26 @@ describe API::MergeRequests do
expect(response_dates).to eq(response_dates.sort)
end
end
+
+ context 'source_branch param' do
+ it 'returns merge requests with the given source branch' do
+ get api('/merge_requests', user), source_branch: merge_request_closed.source_branch, state: 'all'
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |mr| mr['id'] })
+ .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ end
+ end
+
+ context 'target_branch param' do
+ it 'returns merge requests with the given target branch' do
+ get api('/merge_requests', user), target_branch: merge_request_closed.target_branch, state: 'all'
+
+ expect(json_response.length).to eq(2)
+ expect(json_response.map { |mr| mr['id'] })
+ .to contain_exactly(merge_request_closed.id, merge_request_merged.id)
+ end
+ end
end
end
@@ -460,6 +537,45 @@ describe API::MergeRequests do
expect(json_response['changes_count']).to eq(merge_request.merge_request_diff.real_size)
end
+ context 'merge_request_metrics' do
+ before do
+ merge_request.metrics.update!(merged_by: user,
+ latest_closed_by: user,
+ latest_closed_at: 1.hour.ago,
+ merged_at: 2.hours.ago,
+ pipeline: pipeline,
+ latest_build_started_at: 3.hours.ago,
+ latest_build_finished_at: 1.hour.ago,
+ first_deployed_to_production_at: 3.minutes.ago)
+ end
+
+ it 'has fields from merge request metrics' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}", user)
+
+ expect(json_response).to include('merged_by',
+ 'merged_at',
+ 'closed_by',
+ 'closed_at',
+ 'latest_build_started_at',
+ 'latest_build_finished_at',
+ 'first_deployed_to_production_at',
+ 'pipeline')
+ end
+
+ it 'returns correct values' do
+ get api("/projects/#{project.id}/merge_requests/#{merge_request.reload.iid}", user)
+
+ expect(json_response['merged_by']['id']).to eq(merge_request.metrics.merged_by_id)
+ expect(Time.parse json_response['merged_at']).to be_like_time(merge_request.metrics.merged_at)
+ expect(json_response['closed_by']['id']).to eq(merge_request.metrics.latest_closed_by_id)
+ expect(Time.parse json_response['closed_at']).to be_like_time(merge_request.metrics.latest_closed_at)
+ expect(json_response['pipeline']['id']).to eq(merge_request.metrics.pipeline_id)
+ expect(Time.parse json_response['latest_build_started_at']).to be_like_time(merge_request.metrics.latest_build_started_at)
+ expect(Time.parse json_response['latest_build_finished_at']).to be_like_time(merge_request.metrics.latest_build_finished_at)
+ expect(Time.parse json_response['first_deployed_to_production_at']).to be_like_time(merge_request.metrics.first_deployed_to_production_at)
+ end
+ end
+
it "returns a 404 error if merge_request_iid not found" do
get api("/projects/#{project.id}/merge_requests/999", user)
expect(response).to have_gitlab_http_status(404)
diff --git a/spec/requests/api/pages_domains_spec.rb b/spec/requests/api/pages_domains_spec.rb
index 025165622b7..dc3a116c060 100644
--- a/spec/requests/api/pages_domains_spec.rb
+++ b/spec/requests/api/pages_domains_spec.rb
@@ -16,7 +16,7 @@ describe API::PagesDomains do
let(:route) { "/projects/#{project.id}/pages/domains" }
let(:route_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain.domain}" }
- let(:route_domain_path) { "/projects/#{project.path_with_namespace.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" }
+ let(:route_domain_path) { "/projects/#{project.full_path.gsub('/', '%2F')}/pages/domains/#{pages_domain.domain}" }
let(:route_secure_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_secure.domain}" }
let(:route_expired_domain) { "/projects/#{project.id}/pages/domains/#{pages_domain_expired.domain}" }
let(:route_vacant_domain) { "/projects/#{project.id}/pages/domains/www.vacant-domain.test" }
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 1fd082ecc38..392cad667be 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -28,6 +28,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com")
expect(json_response.first['issues_events']).to eq(true)
+ expect(json_response.first['confidential_issues_events']).to eq(true)
expect(json_response.first['push_events']).to eq(true)
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
@@ -56,6 +57,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(hook.push_events)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
@@ -90,13 +92,14 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "adds hook to project" do
expect do
post api("/projects/#{project.id}/hooks", user),
- url: "http://example.com", issues_events: true, wiki_page_events: true,
+ url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true,
job_events: true
end.to change {project.hooks.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true)
+ expect(json_response['confidential_issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true)
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
@@ -144,6 +147,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(false)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
diff --git a/spec/requests/api/runner_spec.rb b/spec/requests/api/runner_spec.rb
index f10b6e43d09..ce1311ac97c 100644
--- a/spec/requests/api/runner_spec.rb
+++ b/spec/requests/api/runner_spec.rb
@@ -122,6 +122,15 @@ describe API::Runner do
end
end
end
+
+ it "sets the runner's ip_address" do
+ post api('/runners'),
+ { token: registration_token },
+ { 'REMOTE_ADDR' => '123.111.123.111' }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(Ci::Runner.first.ip_address).to eq('123.111.123.111')
+ end
end
describe 'DELETE /api/v4/runners' do
@@ -422,6 +431,15 @@ describe API::Runner do
end
end
+ it "sets the runner's ip_address" do
+ post api('/jobs/request'),
+ { token: runner.token },
+ { 'User-Agent' => user_agent, 'REMOTE_ADDR' => '123.222.123.222' }
+
+ expect(response).to have_gitlab_http_status 201
+ expect(runner.reload.ip_address).to eq('123.222.123.222')
+ end
+
context 'when concurrently updating a job' do
before do
expect_any_instance_of(Ci::Build).to receive(:run!)
@@ -682,7 +700,7 @@ describe API::Runner do
context 'when tace is given' do
it 'creates a trace artifact' do
- allow_any_instance_of(BuildFinishedWorker).to receive(:perform).with(job.id) do
+ allow(BuildFinishedWorker).to receive(:perform_async).with(job.id) do
CreateTraceArtifactWorker.new.perform(job.id)
end
@@ -1082,11 +1100,13 @@ describe API::Runner do
context 'posts artifacts file and metadata file' do
let!(:artifacts) { file_upload }
+ let!(:artifacts_sha256) { Digest::SHA256.file(artifacts.path).hexdigest }
let!(:metadata) { file_upload2 }
let(:stored_artifacts_file) { job.reload.artifacts_file.file }
let(:stored_metadata_file) { job.reload.artifacts_metadata.file }
let(:stored_artifacts_size) { job.reload.artifacts_size }
+ let(:stored_artifacts_sha256) { job.reload.job_artifacts_archive.file_sha256 }
before do
post(api("/jobs/#{job.id}/artifacts"), post_data, headers_with_token)
@@ -1096,6 +1116,7 @@ describe API::Runner do
let(:post_data) do
{ 'file.path' => artifacts.path,
'file.name' => artifacts.original_filename,
+ 'file.sha256' => artifacts_sha256,
'metadata.path' => metadata.path,
'metadata.name' => metadata.original_filename }
end
@@ -1105,6 +1126,7 @@ describe API::Runner do
expect(stored_artifacts_file.original_filename).to eq(artifacts.original_filename)
expect(stored_metadata_file.original_filename).to eq(metadata.original_filename)
expect(stored_artifacts_size).to eq(72821)
+ expect(stored_artifacts_sha256).to eq(artifacts_sha256)
end
end
diff --git a/spec/requests/api/v3/issues_spec.rb b/spec/requests/api/v3/issues_spec.rb
index 0e745c82395..11b5469be7b 100644
--- a/spec/requests/api/v3/issues_spec.rb
+++ b/spec/requests/api/v3/issues_spec.rb
@@ -1218,7 +1218,7 @@ describe API::V3::Issues do
context 'when source project does not exist' do
it 'returns 404 when trying to move an issue' do
- post v3_api("/projects/123/issues/#{issue.id}/move", user),
+ post v3_api("/projects/0/issues/#{issue.id}/move", user),
to_project_id: target_project.id
expect(response).to have_gitlab_http_status(404)
@@ -1229,7 +1229,7 @@ describe API::V3::Issues do
context 'when target project does not exist' do
it 'returns 404 when trying to move an issue' do
post v3_api("/projects/#{project.id}/issues/#{issue.id}/move", user),
- to_project_id: 123
+ to_project_id: 0
expect(response).to have_gitlab_http_status(404)
end
diff --git a/spec/requests/api/v3/project_hooks_spec.rb b/spec/requests/api/v3/project_hooks_spec.rb
index 248ae97f875..8f6a2330d25 100644
--- a/spec/requests/api/v3/project_hooks_spec.rb
+++ b/spec/requests/api/v3/project_hooks_spec.rb
@@ -27,6 +27,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(json_response.count).to eq(1)
expect(json_response.first['url']).to eq("http://example.com")
expect(json_response.first['issues_events']).to eq(true)
+ expect(json_response.first['confidential_issues_events']).to eq(true)
expect(json_response.first['push_events']).to eq(true)
expect(json_response.first['merge_requests_events']).to eq(true)
expect(json_response.first['tag_push_events']).to eq(true)
@@ -54,6 +55,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq(hook.url)
expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(hook.push_events)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
@@ -87,12 +89,13 @@ describe API::ProjectHooks, 'ProjectHooks' do
it "adds hook to project" do
expect do
post v3_api("/projects/#{project.id}/hooks", user),
- url: "http://example.com", issues_events: true, wiki_page_events: true, build_events: true
+ url: "http://example.com", issues_events: true, confidential_issues_events: true, wiki_page_events: true, build_events: true
end.to change {project.hooks.count}.by(1)
expect(response).to have_gitlab_http_status(201)
expect(json_response['url']).to eq('http://example.com')
expect(json_response['issues_events']).to eq(true)
+ expect(json_response['confidential_issues_events']).to eq(true)
expect(json_response['push_events']).to eq(true)
expect(json_response['merge_requests_events']).to eq(false)
expect(json_response['tag_push_events']).to eq(false)
@@ -139,6 +142,7 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(200)
expect(json_response['url']).to eq('http://example.org')
expect(json_response['issues_events']).to eq(hook.issues_events)
+ expect(json_response['confidential_issues_events']).to eq(hook.confidential_issues_events)
expect(json_response['push_events']).to eq(false)
expect(json_response['merge_requests_events']).to eq(hook.merge_requests_events)
expect(json_response['tag_push_events']).to eq(hook.tag_push_events)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index c6fdda203ad..6dbbb1ad7bb 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -506,8 +506,8 @@ describe 'Git HTTP requests' do
context 'when LDAP is configured' do
before do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
- allow_any_instance_of(Gitlab::LDAP::Authentication)
+ allow(Gitlab::Auth::LDAP::Config).to receive(:enabled?).and_return(true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Authentication)
.to receive(:login).and_return(nil)
end
@@ -597,7 +597,7 @@ describe 'Git HTTP requests' do
context "when a gitlab ci token is provided" do
let(:project) { create(:project, :repository) }
let(:build) { create(:ci_build, :running) }
- let(:other_project) { create(:project) }
+ let(:other_project) { create(:project, :repository) }
before do
build.update!(project: project) # can't associate it on factory create
@@ -648,10 +648,10 @@ describe 'Git HTTP requests' do
context 'when the repo does not exist' do
let(:project) { create(:project) }
- it 'rejects pulls with 403 Forbidden' do
+ it 'rejects pulls with 404 Not Found' do
clone_get path, env
- expect(response).to have_gitlab_http_status(:forbidden)
+ expect(response).to have_gitlab_http_status(:not_found)
expect(response.body).to eq(git_access_error(:no_repo))
end
end
@@ -795,9 +795,9 @@ describe 'Git HTTP requests' do
let(:path) { 'doesnt/exist.git' }
before do
- allow(Gitlab::LDAP::Config).to receive(:enabled?).and_return(true)
- allow(Gitlab::LDAP::Authentication).to receive(:login).and_return(nil)
- allow(Gitlab::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
+ allow(Gitlab::Auth::OAuth::Provider).to receive(:enabled?).and_return(true)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).and_return(nil)
+ allow_any_instance_of(Gitlab::Auth::LDAP::Authentication).to receive(:login).with(user.username, user.password).and_return(user)
end
it_behaves_like 'pulls require Basic HTTP Authentication'
diff --git a/spec/requests/openid_connect_spec.rb b/spec/requests/openid_connect_spec.rb
index de829011e58..6bed8e812c0 100644
--- a/spec/requests/openid_connect_spec.rb
+++ b/spec/requests/openid_connect_spec.rb
@@ -68,10 +68,10 @@ describe 'OpenID Connect requests' do
let!(:public_email) { build :email, email: 'public@example.com' }
let!(:private_email) { build :email, email: 'private@example.com' }
- let!(:group1) { create :group, path: 'group1' }
- let!(:group2) { create :group, path: 'group2' }
- let!(:group3) { create :group, path: 'group3', parent: group2 }
- let!(:group4) { create :group, path: 'group4', parent: group3 }
+ let!(:group1) { create :group }
+ let!(:group2) { create :group }
+ let!(:group3) { create :group, parent: group2 }
+ let!(:group4) { create :group, parent: group3 }
before do
group1.add_user(user, GroupMember::OWNER)
@@ -93,8 +93,8 @@ describe 'OpenID Connect requests' do
'groups' => anything
}))
- expected_groups = %w[group1 group2/group3]
- expected_groups << 'group2/group3/group4' if Group.supports_nested_groups?
+ expected_groups = [group1.full_path, group3.full_path]
+ expected_groups << group4.full_path if Group.supports_nested_groups?
expect(json_response['groups']).to match_array(expected_groups)
end
end
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index 98f70e2101b..eef860821e5 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -15,7 +15,7 @@ describe 'cycle analytics events' do
end
end
- deploy_master
+ deploy_master(user, project)
login_as(user)
end
@@ -119,7 +119,7 @@ describe 'cycle analytics events' do
def create_cycle
milestone = create(:milestone, project: project)
issue.update(milestone: milestone)
- mr = create_merge_request_closing_issue(issue, commit_message: "References #{issue.to_reference}")
+ mr = create_merge_request_closing_issue(user, project, issue, commit_message: "References #{issue.to_reference}")
pipeline = create(:ci_empty_pipeline, status: 'created', project: project, ref: mr.source_branch, sha: mr.source_branch_sha, head_pipeline_of: mr)
pipeline.run
@@ -127,7 +127,7 @@ describe 'cycle analytics events' do
create(:ci_build, pipeline: pipeline, status: :success, author: user)
create(:ci_build, pipeline: pipeline, status: :success, author: user)
- merge_merge_requests_closing_issue(issue)
+ merge_merge_requests_closing_issue(user, project, issue)
ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
end
diff --git a/spec/serializers/cluster_application_entity_spec.rb b/spec/serializers/cluster_application_entity_spec.rb
index b5a55b4ef6e..852b6af9f7f 100644
--- a/spec/serializers/cluster_application_entity_spec.rb
+++ b/spec/serializers/cluster_application_entity_spec.rb
@@ -26,5 +26,19 @@ describe ClusterApplicationEntity do
expect(subject[:status_reason]).to eq(application.status_reason)
end
end
+
+ context 'for ingress application' do
+ let(:application) do
+ build(
+ :clusters_applications_ingress,
+ :installed,
+ external_ip: '111.222.111.222'
+ )
+ end
+
+ it 'includes external_ip' do
+ expect(subject[:external_ip]).to eq('111.222.111.222')
+ end
+ end
end
end
diff --git a/spec/serializers/diff_file_entity_spec.rb b/spec/serializers/diff_file_entity_spec.rb
new file mode 100644
index 00000000000..45d7c703df3
--- /dev/null
+++ b/spec/serializers/diff_file_entity_spec.rb
@@ -0,0 +1,24 @@
+require 'spec_helper'
+
+describe DiffFileEntity do
+ include RepoHelpers
+
+ let(:project) { create(:project, :repository) }
+ let(:repository) { project.repository }
+ let(:commit) { project.commit(sample_commit.id) }
+ let(:diff_refs) { commit.diff_refs }
+ let(:diff) { commit.raw_diffs.first }
+ let(:diff_file) { Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
+ let(:entity) { described_class.new(diff_file) }
+
+ subject { entity.as_json }
+
+ it 'exposes correct attributes' do
+ expect(subject).to include(
+ :submodule, :submodule_link, :file_path,
+ :deleted_file, :old_path, :new_path, :mode_changed,
+ :a_mode, :b_mode, :text, :old_path_html,
+ :new_path_html
+ )
+ end
+end
diff --git a/spec/serializers/discussion_entity_spec.rb b/spec/serializers/discussion_entity_spec.rb
new file mode 100644
index 00000000000..7ee8e38af1c
--- /dev/null
+++ b/spec/serializers/discussion_entity_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+
+describe DiscussionEntity do
+ include RepoHelpers
+
+ let(:user) { create(:user) }
+ let(:note) { create(:discussion_note_on_merge_request) }
+ let(:discussion) { note.discussion }
+ let(:request) { double('request') }
+ let(:controller) { double('controller') }
+ let(:entity) { described_class.new(discussion, request: request, context: controller) }
+
+ subject { entity.as_json }
+
+ before do
+ allow(controller).to receive(:render_to_string)
+ allow(request).to receive(:current_user).and_return(user)
+ allow(request).to receive(:noteable).and_return(note.noteable)
+ end
+
+ it 'exposes correct attributes' do
+ expect(subject).to include(
+ :id, :expanded, :notes, :individual_note,
+ :resolvable, :resolved, :resolve_path,
+ :resolve_with_issue_path, :diff_discussion
+ )
+ end
+
+ context 'when diff file is present' do
+ let(:note) { create(:diff_note_on_merge_request) }
+
+ it 'exposes diff file attributes' do
+ expect(subject).to include(:diff_file, :truncated_diff_lines, :image_diff_html)
+ end
+ end
+end
diff --git a/spec/serializers/note_entity_spec.rb b/spec/serializers/note_entity_spec.rb
index 3459cc72063..51a8587ace9 100644
--- a/spec/serializers/note_entity_spec.rb
+++ b/spec/serializers/note_entity_spec.rb
@@ -48,4 +48,15 @@ describe NoteEntity do
expect(subject).to include(:system_note_icon_name)
end
end
+
+ context 'when note is part of resolvable discussion' do
+ before do
+ allow(note).to receive(:part_of_discussion?).and_return(true)
+ allow(note).to receive(:resolvable?).and_return(true)
+ end
+
+ it 'exposes paths to resolve note' do
+ expect(subject).to include(:resolve_path, :resolve_with_issue_path)
+ end
+ end
end
diff --git a/spec/services/auth/container_registry_authentication_service_spec.rb b/spec/services/auth/container_registry_authentication_service_spec.rb
index 9128280eb5a..290eeae828e 100644
--- a/spec/services/auth/container_registry_authentication_service_spec.rb
+++ b/spec/services/auth/container_registry_authentication_service_spec.rb
@@ -172,7 +172,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -200,7 +200,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -239,7 +239,7 @@ describe Auth::ContainerRegistryAuthenticationService do
end
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -270,7 +270,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to delete images' do
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -311,7 +311,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to delete images' do
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -323,7 +323,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to pull or push images' do
let(:current_user) { create(:user, external: true) }
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:pull,push" }
+ { scope: "repository:#{project.full_path}:pull,push" }
end
it_behaves_like 'an inaccessible'
@@ -333,7 +333,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow anyone to delete images' do
let(:current_user) { create(:user, external: true) }
let(:current_params) do
- { scope: "repository:#{project.path_with_namespace}:*" }
+ { scope: "repository:#{project.full_path}:*" }
end
it_behaves_like 'an inaccessible'
@@ -359,7 +359,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'allow to delete images' do
let(:current_params) do
- { scope: "repository:#{current_project.path_with_namespace}:*" }
+ { scope: "repository:#{current_project.full_path}:*" }
end
it_behaves_like 'a deletable' do
@@ -398,7 +398,7 @@ describe Auth::ContainerRegistryAuthenticationService do
context 'disallow to delete images' do
let(:current_params) do
- { scope: "repository:#{current_project.path_with_namespace}:*" }
+ { scope: "repository:#{current_project.full_path}:*" }
end
it_behaves_like 'an inaccessible' do
diff --git a/spec/services/chat_names/find_user_service_spec.rb b/spec/services/chat_names/find_user_service_spec.rb
index 79aaac3aeb6..5734b10109a 100644
--- a/spec/services/chat_names/find_user_service_spec.rb
+++ b/spec/services/chat_names/find_user_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe ChatNames::FindUserService do
+describe ChatNames::FindUserService, :clean_gitlab_redis_shared_state do
describe '#execute' do
let(:service) { create(:service) }
@@ -13,21 +13,30 @@ describe ChatNames::FindUserService do
context 'when existing user is requested' do
let(:params) { { team_id: chat_name.team_id, user_id: chat_name.chat_id } }
- it 'returns the existing user' do
- is_expected.to eq(user)
+ it 'returns the existing chat_name' do
+ is_expected.to eq(chat_name)
end
- it 'updates when last time chat name was used' do
+ it 'updates the last used timestamp if one is not already set' do
expect(chat_name.last_used_at).to be_nil
subject
- initial_last_used = chat_name.reload.last_used_at
- expect(initial_last_used).to be_present
+ expect(chat_name.reload.last_used_at).to be_present
+ end
+
+ it 'only updates an existing timestamp once within a certain time frame' do
+ service = described_class.new(service, params)
+
+ expect(chat_name.last_used_at).to be_nil
+
+ service.execute
+
+ time = chat_name.reload.last_used_at
- Timecop.travel(2.days.from_now) { described_class.new(service, params).execute }
+ service.execute
- expect(chat_name.reload.last_used_at).to be > initial_last_used
+ expect(chat_name.reload.last_used_at).to eq(time)
end
end
diff --git a/spec/services/ci/create_trace_artifact_service_spec.rb b/spec/services/ci/create_trace_artifact_service_spec.rb
index 847a88920fe..8c5e8e438c7 100644
--- a/spec/services/ci/create_trace_artifact_service_spec.rb
+++ b/spec/services/ci/create_trace_artifact_service_spec.rb
@@ -4,40 +4,60 @@ describe Ci::CreateTraceArtifactService do
describe '#execute' do
subject { described_class.new(nil, nil).execute(job) }
- let(:job) { create(:ci_build) }
-
context 'when the job does not have trace artifact' do
context 'when the job has a trace file' do
- before do
- allow_any_instance_of(Gitlab::Ci::Trace)
- .to receive(:default_path) { expand_fixture_path('trace/sample_trace') }
+ let!(:job) { create(:ci_build, :trace_live) }
+ let!(:legacy_path) { job.trace.read { |stream| return stream.path } }
+ let!(:legacy_checksum) { Digest::SHA256.file(legacy_path).hexdigest }
+ let(:new_path) { job.job_artifacts_trace.file.path }
+ let(:new_checksum) { Digest::SHA256.file(new_path).hexdigest }
- allow_any_instance_of(JobArtifactUploader).to receive(:move_to_cache) { false }
- allow_any_instance_of(JobArtifactUploader).to receive(:move_to_store) { false }
- end
+ it { expect(File.exist?(legacy_path)).to be_truthy }
it 'creates trace artifact' do
expect { subject }.to change { Ci::JobArtifact.count }.by(1)
- expect(job.job_artifacts_trace.read_attribute(:file)).to eq('sample_trace')
+ expect(File.exist?(legacy_path)).to be_falsy
+ expect(File.exist?(new_path)).to be_truthy
+ expect(new_checksum).to eq(legacy_checksum)
+ expect(job.job_artifacts_trace.file.exists?).to be_truthy
+ expect(job.job_artifacts_trace.file.filename).to eq('job.log')
end
- context 'when the job has already had trace artifact' do
+ context 'when failed to create trace artifact record' do
before do
- create(:ci_job_artifact, :trace, job: job)
+ # When ActiveRecord error happens
+ allow_any_instance_of(Ci::JobArtifact).to receive(:save).and_return(false)
+ allow_any_instance_of(Ci::JobArtifact).to receive_message_chain(:errors, :full_messages)
+ .and_return("Error")
+
+ subject rescue nil
+
+ job.reload
end
- it 'does not create trace artifact' do
- expect { subject }.not_to change { Ci::JobArtifact.count }
+ it 'keeps legacy trace and removes trace artifact' do
+ expect(File.exist?(legacy_path)).to be_truthy
+ expect(job.job_artifacts_trace).to be_nil
end
end
end
context 'when the job does not have a trace file' do
+ let!(:job) { create(:ci_build) }
+
it 'does not create trace artifact' do
expect { subject }.not_to change { Ci::JobArtifact.count }
end
end
end
+
+ context 'when the job has already had trace artifact' do
+ let!(:job) { create(:ci_build, :trace_artifact) }
+
+ it 'does not create trace artifact' do
+ expect { subject }.not_to change { Ci::JobArtifact.count }
+ end
+ end
end
end
diff --git a/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
new file mode 100644
index 00000000000..bf038595a4d
--- /dev/null
+++ b/spec/services/clusters/applications/check_ingress_ip_address_service_spec.rb
@@ -0,0 +1,73 @@
+require 'spec_helper'
+
+describe Clusters::Applications::CheckIngressIpAddressService do
+ let(:application) { create(:clusters_applications_ingress, :installed) }
+ let(:service) { described_class.new(application) }
+ let(:kubeclient) { double(::Kubeclient::Client, get_service: kube_service) }
+ let(:ingress) { [{ ip: '111.222.111.222' }] }
+ let(:exclusive_lease) { instance_double(Gitlab::ExclusiveLease, try_obtain: true) }
+
+ let(:kube_service) do
+ ::Kubeclient::Resource.new(
+ {
+ status: {
+ loadBalancer: {
+ ingress: ingress
+ }
+ }
+ }
+ )
+ end
+
+ subject { service.execute }
+
+ before do
+ allow(application.cluster).to receive(:kubeclient).and_return(kubeclient)
+ allow(Gitlab::ExclusiveLease)
+ .to receive(:new)
+ .with("check_ingress_ip_address_service:#{application.id}", timeout: 15.seconds.to_i)
+ .and_return(exclusive_lease)
+ end
+
+ describe '#execute' do
+ context 'when the ingress ip address is available' do
+ it 'updates the external_ip for the app' do
+ subject
+
+ expect(application.external_ip).to eq('111.222.111.222')
+ end
+ end
+
+ context 'when the ingress ip address is not available' do
+ let(:ingress) { nil }
+
+ it 'does not error' do
+ subject
+ end
+ end
+
+ context 'when the exclusive lease cannot be obtained' do
+ before do
+ allow(exclusive_lease)
+ .to receive(:try_obtain)
+ .and_return(false)
+ end
+
+ it 'does not call kubeclient' do
+ subject
+
+ expect(kubeclient).not_to have_received(:get_service)
+ end
+ end
+
+ context 'when there is already an external_ip' do
+ let(:application) { create(:clusters_applications_ingress, :installed, external_ip: '001.111.002.111') }
+
+ it 'does not call kubeclient' do
+ subject
+
+ expect(kubeclient).not_to have_received(:get_service)
+ end
+ end
+ end
+end
diff --git a/spec/services/labels/find_or_create_service_spec.rb b/spec/services/labels/find_or_create_service_spec.rb
index 78aa5d442e7..68d5660445a 100644
--- a/spec/services/labels/find_or_create_service_spec.rb
+++ b/spec/services/labels/find_or_create_service_spec.rb
@@ -15,47 +15,79 @@ describe Labels::FindOrCreateService do
context 'when acting on behalf of a specific user' do
let(:user) { create(:user) }
- subject(:service) { described_class.new(user, project, params) }
- before do
- project.add_developer(user)
- end
- context 'when label does not exist at group level' do
- it 'creates a new label at project level' do
- expect { service.execute }.to change(project.labels, :count).by(1)
+ context 'when finding labels on project level' do
+ subject(:service) { described_class.new(user, project, params) }
+
+ before do
+ project.add_developer(user)
end
- end
- context 'when label exists at group level' do
- it 'returns the group label' do
- group_label = create(:group_label, group: group, title: 'Security')
+ context 'when label does not exist at group level' do
+ it 'creates a new label at project level' do
+ expect { service.execute }.to change(project.labels, :count).by(1)
+ end
+ end
- expect(service.execute).to eq group_label
+ context 'when label exists at group level' do
+ it 'returns the group label' do
+ group_label = create(:group_label, group: group, title: 'Security')
+
+ expect(service.execute).to eq group_label
+ end
+ end
+
+ context 'when label exists at project level' do
+ it 'returns the project label' do
+ project_label = create(:label, project: project, title: 'Security')
+
+ expect(service.execute).to eq project_label
+ end
end
end
- context 'when label does not exist at group level' do
- it 'creates a new label at project leve' do
- expect { service.execute }.to change(project.labels, :count).by(1)
+ context 'when finding labels on group level' do
+ subject(:service) { described_class.new(user, group, params) }
+
+ before do
+ group.add_developer(user)
+ end
+
+ context 'when label does not exist at group level' do
+ it 'creates a new label at group level' do
+ expect { service.execute }.to change(group.labels, :count).by(1)
+ end
+ end
+
+ context 'when label exists at group level' do
+ it 'returns the group label' do
+ group_label = create(:group_label, group: group, title: 'Security')
+
+ expect(service.execute).to eq group_label
+ end
end
end
+ end
+
+ context 'when authorization is not required' do
+ context 'when finding labels on project level' do
+ subject(:service) { described_class.new(nil, project, params) }
- context 'when label exists at project level' do
it 'returns the project label' do
project_label = create(:label, project: project, title: 'Security')
- expect(service.execute).to eq project_label
+ expect(service.execute(skip_authorization: true)).to eq project_label
end
end
- end
- context 'when authorization is not required' do
- subject(:service) { described_class.new(nil, project, params) }
+ context 'when finding labels on group level' do
+ subject(:service) { described_class.new(nil, group, params) }
- it 'returns the project label' do
- project_label = create(:label, project: project, title: 'Security')
+ it 'returns the group label' do
+ group_label = create(:group_label, group: group, title: 'Security')
- expect(service.execute(skip_authorization: true)).to eq project_label
+ expect(service.execute(skip_authorization: true)).to eq group_label
+ end
end
end
end
diff --git a/spec/services/members/approve_access_request_service_spec.rb b/spec/services/members/approve_access_request_service_spec.rb
index b3018169a1c..7076571b753 100644
--- a/spec/services/members/approve_access_request_service_spec.rb
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -1,70 +1,56 @@
require 'spec_helper'
describe Members::ApproveAccessRequestService do
- let(:user) { create(:user) }
- let(:access_requester) { create(:user) }
let(:project) { create(:project, :public, :access_requestable) }
let(:group) { create(:group, :public, :access_requestable) }
+ let(:current_user) { create(:user) }
+ let(:access_requester_user) { create(:user) }
+ let(:access_requester) { source.requesters.find_by!(user_id: access_requester_user.id) }
let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
- expect { described_class.new(source, user, params).execute(opts) }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(source, user, params).execute(opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(current_user).execute(access_requester, opts) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service approving an access request' do
it 'succeeds' do
- expect { described_class.new(source, user, params).execute(opts) }.to change { source.requesters.count }.by(-1)
+ expect { described_class.new(current_user).execute(access_requester, opts) }.to change { source.requesters.count }.by(-1)
end
it 'returns a <Source>Member' do
- member = described_class.new(source, user, params).execute(opts)
+ member = described_class.new(current_user).execute(access_requester, opts)
expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_nil
end
context 'with a custom access level' do
- let(:params2) { params.merge(user_id: access_requester.id, access_level: Gitlab::Access::MASTER) }
-
it 'returns a ProjectMember with the custom access level' do
- member = described_class.new(source, user, params2).execute(opts)
+ member = described_class.new(current_user, access_level: Gitlab::Access::MASTER).execute(access_requester, opts)
- expect(member.access_level).to eq Gitlab::Access::MASTER
+ expect(member.access_level).to eq(Gitlab::Access::MASTER)
end
end
end
- context 'when no access requester are found' do
- let(:params) { { user_id: 42 } }
-
- it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
- let(:source) { project }
- end
-
- it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
- let(:source) { group }
- end
- end
-
context 'when an access requester is found' do
before do
- project.request_access(access_requester)
- group.request_access(access_requester)
+ project.request_access(access_requester_user)
+ group.request_access(access_requester_user)
end
- let(:params) { { user_id: access_requester.id } }
context 'when current user is nil' do
let(:user) { nil }
- context 'and :force option is not given' do
+ context 'and :ldap option is not given' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
end
@@ -74,8 +60,8 @@ describe Members::ApproveAccessRequestService do
end
end
- context 'and :force option is false' do
- let(:opts) { { force: false } }
+ context 'and :skip_authorization option is false' do
+ let(:opts) { { skip_authorization: false } }
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
let(:source) { project }
@@ -86,8 +72,8 @@ describe Members::ApproveAccessRequestService do
end
end
- context 'and :force option is true' do
- let(:opts) { { force: true } }
+ context 'and :skip_authorization option is true' do
+ let(:opts) { { skip_authorization: true } }
it_behaves_like 'a service approving an access request' do
let(:source) { project }
@@ -97,18 +83,6 @@ describe Members::ApproveAccessRequestService do
let(:source) { group }
end
end
-
- context 'and :force param is true' do
- let(:params) { { user_id: access_requester.id, force: true } }
-
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
- end
-
- it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
- end
- end
end
context 'when current user cannot approve access request to the project' do
@@ -123,8 +97,8 @@ describe Members::ApproveAccessRequestService do
context 'when current user can approve access request to the project' do
before do
- project.add_master(user)
- group.add_owner(user)
+ project.add_master(current_user)
+ group.add_owner(current_user)
end
it_behaves_like 'a service approving an access request' do
@@ -134,14 +108,6 @@ describe Members::ApproveAccessRequestService do
it_behaves_like 'a service approving an access request' do
let(:source) { group }
end
-
- context 'when given a :id' do
- let(:params) { { id: project.requesters.find_by!(user_id: access_requester.id).id } }
-
- it_behaves_like 'a service approving an access request' do
- let(:source) { project }
- end
- end
end
end
end
diff --git a/spec/services/members/authorized_destroy_service_spec.rb b/spec/services/members/authorized_destroy_service_spec.rb
deleted file mode 100644
index 9cf6f64a078..00000000000
--- a/spec/services/members/authorized_destroy_service_spec.rb
+++ /dev/null
@@ -1,110 +0,0 @@
-require 'spec_helper'
-
-describe Members::AuthorizedDestroyService do
- let(:member_user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:group) { create(:group, :public) }
- let(:group_project) { create(:project, :public, group: group) }
-
- def number_of_assigned_issuables(user)
- Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count
- end
-
- context 'Invited users' do
- # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504
- it 'destroys invited project member' do
- project.add_developer(member_user)
-
- member = create :project_member, :invited, project: project
-
- expect { described_class.new(member, member_user).execute }
- .to change { Member.count }.from(3).to(2)
- end
-
- it "doesn't destroy invited project member notification_settings" do
- project.add_developer(member_user)
-
- member = create :project_member, :invited, project: project
-
- expect { described_class.new(member, member_user).execute }
- .not_to change { NotificationSetting.count }
- end
-
- it 'destroys invited group member' do
- group.add_developer(member_user)
-
- member = create :group_member, :invited, group: group
-
- expect { described_class.new(member, member_user).execute }
- .to change { Member.count }.from(2).to(1)
- end
-
- it "doesn't destroy invited group member notification_settings" do
- group.add_developer(member_user)
-
- member = create :group_member, :invited, group: group
-
- expect { described_class.new(member, member_user).execute }
- .not_to change { NotificationSetting.count }
- end
- end
-
- context 'Requested user' do
- it "doesn't destroy member notification_settings" do
- member = create(:project_member, user: member_user, requested_at: Time.now)
-
- expect { described_class.new(member, member_user).execute }
- .not_to change { NotificationSetting.count }
- end
- end
-
- context 'Group member' do
- let(:member) { group.members.find_by(user_id: member_user.id) }
-
- before do
- group.add_developer(member_user)
- end
-
- it "unassigns issues and merge requests" do
- issue = create :issue, project: group_project, assignees: [member_user]
- create :issue, assignees: [member_user]
- merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
- create :merge_request, target_project: project, source_project: project, assignee: member_user
-
- expect { described_class.new(member, member_user).execute }
- .to change { number_of_assigned_issuables(member_user) }.from(4).to(2)
-
- expect(issue.reload.assignee_ids).to be_empty
- expect(merge_request.reload.assignee_id).to be_nil
- end
-
- it 'destroys member notification_settings' do
- group.add_developer(member_user)
- member = group.members.find_by(user_id: member_user.id)
-
- expect { described_class.new(member, member_user).execute }
- .to change { member_user.notification_settings.count }.by(-1)
- end
- end
-
- context 'Project member' do
- let(:member) { project.members.find_by(user_id: member_user.id) }
-
- before do
- project.add_developer(member_user)
- end
-
- it "unassigns issues and merge requests" do
- create :issue, project: project, assignees: [member_user]
- create :merge_request, target_project: project, source_project: project, assignee: member_user
-
- expect { described_class.new(member, member_user).execute }
- .to change { number_of_assigned_issuables(member_user) }.from(2).to(0)
- end
-
- it 'destroys member notification_settings' do
- expect { described_class.new(member, member_user).execute }
- .to change { member_user.notification_settings.count }.by(-1)
- end
- end
-end
diff --git a/spec/services/members/create_service_spec.rb b/spec/services/members/create_service_spec.rb
index 6bd4718e780..1831c62d788 100644
--- a/spec/services/members/create_service_spec.rb
+++ b/spec/services/members/create_service_spec.rb
@@ -11,7 +11,7 @@ describe Members::CreateService do
it 'adds user to members' do
params = { user_ids: project_user.id.to_s, access_level: Gitlab::Access::GUEST }
- result = described_class.new(project, user, params).execute
+ result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:success)
expect(project.users).to include project_user
@@ -19,7 +19,7 @@ describe Members::CreateService do
it 'adds no user to members' do
params = { user_ids: '', access_level: Gitlab::Access::GUEST }
- result = described_class.new(project, user, params).execute
+ result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message]).to be_present
@@ -30,7 +30,7 @@ describe Members::CreateService do
user_ids = 1.upto(101).to_a.join(',')
params = { user_ids: user_ids, access_level: Gitlab::Access::GUEST }
- result = described_class.new(project, user, params).execute
+ result = described_class.new(user, params).execute(project)
expect(result[:status]).to eq(:error)
expect(result[:message]).to be_present
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 91152df3ad9..10c264a90c5 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -1,112 +1,202 @@
require 'spec_helper'
describe Members::DestroyService do
- let(:user) { create(:user) }
+ let(:current_user) { create(:user) }
let(:member_user) { create(:user) }
- let(:project) { create(:project, :public) }
let(:group) { create(:group, :public) }
+ let(:group_project) { create(:project, :public, group: group) }
+ let(:opts) { {} }
shared_examples 'a service raising ActiveRecord::RecordNotFound' do
it 'raises ActiveRecord::RecordNotFound' do
- expect { described_class.new(source, user, params).execute }.to raise_error(ActiveRecord::RecordNotFound)
+ expect { described_class.new(current_user).execute(member) }.to raise_error(ActiveRecord::RecordNotFound)
end
end
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(source, user, params).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(current_user).execute(member) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
+ def number_of_assigned_issuables(user)
+ Issue.assigned_to(user).count + MergeRequest.assigned_to(user).count
+ end
+
shared_examples 'a service destroying a member' do
it 'destroys the member' do
- expect { described_class.new(source, user, params).execute }.to change { source.members.count }.by(-1)
+ expect { described_class.new(current_user).execute(member, opts) }.to change { member.source.members_and_requesters.count }.by(-1)
end
- context 'when the given member is an access requester' do
- before do
- source.members.find_by(user_id: member_user).destroy
- source.update_attributes(request_access_enabled: true)
- source.request_access(member_user)
+ it 'unassigns issues and merge requests' do
+ if member.invite?
+ expect { described_class.new(current_user).execute(member, opts) }
+ .not_to change { number_of_assigned_issuables(member_user) }
+ else
+ create :issue, assignees: [member_user]
+ issue = create :issue, project: group_project, assignees: [member_user]
+ merge_request = create :merge_request, target_project: group_project, source_project: group_project, assignee: member_user
+
+ expect { described_class.new(current_user).execute(member, opts) }
+ .to change { number_of_assigned_issuables(member_user) }.from(3).to(1)
+
+ expect(issue.reload.assignee_ids).to be_empty
+ expect(merge_request.reload.assignee_id).to be_nil
end
- let(:access_requester) { source.requesters.find_by(user_id: member_user) }
+ end
- it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
+ it 'destroys member notification_settings' do
+ if member_user.notification_settings.any?
+ expect { described_class.new(current_user).execute(member, opts) }
+ .to change { member_user.notification_settings.count }.by(-1)
+ else
+ expect { described_class.new(current_user).execute(member, opts) }
+ .not_to change { member_user.notification_settings.count }
+ end
+ end
+ end
- %i[requesters all].each do |scope|
- context "and #{scope} scope is passed" do
- it 'destroys the access requester' do
- expect { described_class.new(source, user, params).execute(scope) }.to change { source.requesters.count }.by(-1)
- end
+ shared_examples 'a service destroying an access requester' do
+ it_behaves_like 'a service destroying a member'
- it 'calls Member#after_decline_request' do
- expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
+ it 'calls Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
- described_class.new(source, user, params).execute(scope)
- end
+ described_class.new(current_user).execute(member)
+ end
- context 'when current user is the member' do
- it 'does not call Member#after_decline_request' do
- expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(access_requester)
+ context 'when current user is the member' do
+ it 'does not call Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
- described_class.new(source, member_user, params).execute(scope)
- end
- end
- end
+ described_class.new(member_user).execute(member)
end
end
end
- context 'when no member are found' do
- let(:params) { { user_id: 42 } }
+ context 'with a member' do
+ before do
+ group_project.add_developer(member_user)
+ group.add_developer(member_user)
+ end
+
+ context 'when current user cannot destroy the given member' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:member) { group_project.members.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying a member' do
+ let(:opts) { { skip_authorization: true } }
+ let(:member) { group_project.members.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:member) { group.members.find_by(user_id: member_user.id) }
+ end
- it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
- let(:source) { project }
+ it_behaves_like 'a service destroying a member' do
+ let(:opts) { { skip_authorization: true } }
+ let(:member) { group.members.find_by(user_id: member_user.id) }
+ end
end
- it_behaves_like 'a service raising ActiveRecord::RecordNotFound' do
- let(:source) { group }
+ context 'when current user can destroy the given member' do
+ before do
+ group_project.add_master(current_user)
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'a service destroying a member' do
+ let(:member) { group_project.members.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying a member' do
+ let(:member) { group.members.find_by(user_id: member_user.id) }
+ end
end
end
- context 'when a member is found' do
+ context 'with an access requester' do
before do
- project.add_developer(member_user)
- group.add_developer(member_user)
+ group_project.update_attributes(request_access_enabled: true)
+ group.update_attributes(request_access_enabled: true)
+ group_project.request_access(member_user)
+ group.request_access(member_user)
end
- let(:params) { { user_id: member_user.id } }
- context 'when current user cannot destroy the given member' do
+ context 'when current user cannot destroy the given access requester' do
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { project }
+ let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying a member' do
+ let(:opts) { { skip_authorization: true } }
+ let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
end
it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
- let(:source) { group }
+ let(:member) { group.requesters.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying a member' do
+ let(:opts) { { skip_authorization: true } }
+ let(:member) { group.requesters.find_by(user_id: member_user.id) }
end
end
- context 'when current user can destroy the given member' do
+ context 'when current user can destroy the given access requester' do
before do
- project.add_master(user)
- group.add_owner(user)
+ group_project.add_master(current_user)
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'a service destroying an access requester' do
+ let(:member) { group_project.requesters.find_by(user_id: member_user.id) }
+ end
+
+ it_behaves_like 'a service destroying an access requester' do
+ let(:member) { group.requesters.find_by(user_id: member_user.id) }
+ end
+ end
+ end
+
+ context 'with an invited user' do
+ let(:project_invited_member) { create(:project_member, :invited, project: group_project) }
+ let(:group_invited_member) { create(:group_member, :invited, group: group) }
+
+ context 'when current user cannot destroy the given invited user' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:member) { project_invited_member }
end
it_behaves_like 'a service destroying a member' do
- let(:source) { project }
+ let(:opts) { { skip_authorization: true } }
+ let(:member) { project_invited_member }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:member) { group_invited_member }
end
it_behaves_like 'a service destroying a member' do
- let(:source) { group }
+ let(:opts) { { skip_authorization: true } }
+ let(:member) { group_invited_member }
end
+ end
- context 'when given a :id' do
- let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
+ context 'when current user can destroy the given invited user' do
+ before do
+ group_project.add_master(current_user)
+ group.add_owner(current_user)
+ end
- it 'destroys the member' do
- expect { described_class.new(project, user, params).execute }
- .to change { project.members.count }.by(-1)
- end
+ # Regression spec for issue: https://gitlab.com/gitlab-org/gitlab-ce/issues/32504
+ it_behaves_like 'a service destroying a member' do
+ let(:member) { project_invited_member }
+ end
+
+ it_behaves_like 'a service destroying a member' do
+ let(:member) { group_invited_member }
end
end
end
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
index 0a704bba521..e93ba5a85c0 100644
--- a/spec/services/members/request_access_service_spec.rb
+++ b/spec/services/members/request_access_service_spec.rb
@@ -5,17 +5,17 @@ describe Members::RequestAccessService do
shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
it 'raises Gitlab::Access::AccessDeniedError' do
- expect { described_class.new(source, user).execute }.to raise_error(Gitlab::Access::AccessDeniedError)
+ expect { described_class.new(user).execute(source) }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
shared_examples 'a service creating a access request' do
it 'succeeds' do
- expect { described_class.new(source, user).execute }.to change { source.requesters.count }.by(1)
+ expect { described_class.new(user).execute(source) }.to change { source.requesters.count }.by(1)
end
it 'returns a <Source>Member' do
- member = described_class.new(source, user).execute
+ member = described_class.new(user).execute(source)
expect(member).to be_a "#{source.class}Member".constantize
expect(member.requested_at).to be_present
diff --git a/spec/services/members/update_service_spec.rb b/spec/services/members/update_service_spec.rb
new file mode 100644
index 00000000000..a451272dd1f
--- /dev/null
+++ b/spec/services/members/update_service_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe Members::UpdateService do
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
+ let(:current_user) { create(:user) }
+ let(:member_user) { create(:user) }
+ let(:permission) { :update }
+ let(:member) { source.members_and_requesters.find_by!(user_id: member_user.id) }
+ let(:params) do
+ { access_level: Gitlab::Access::MASTER }
+ end
+
+ shared_examples 'a service raising Gitlab::Access::AccessDeniedError' do
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect { described_class.new(current_user, params).execute(member, permission: permission) }
+ .to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ shared_examples 'a service updating a member' do
+ it 'updates the member' do
+ updated_member = described_class.new(current_user, params).execute(member, permission: permission)
+
+ expect(updated_member).to be_valid
+ expect(updated_member.access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ before do
+ project.add_developer(member_user)
+ group.add_developer(member_user)
+ end
+
+ context 'when current user cannot update the given member' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can update the given member' do
+ before do
+ project.add_master(current_user)
+ group.add_owner(current_user)
+ end
+
+ it_behaves_like 'a service updating a member' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service updating a member' do
+ let(:source) { group }
+ end
+ end
+end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 3a935d98540..6aed481939e 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -15,8 +15,8 @@ describe MergeRequests::BuildService do
let(:target_branch) { 'master' }
let(:merge_request) { service.execute }
let(:compare) { double(:compare, commits: commits) }
- let(:commit_1) { double(:commit_1, safe_message: "Initial commit\n\nCreate the app") }
- let(:commit_2) { double(:commit_2, safe_message: 'This is a bad commit message!') }
+ let(:commit_1) { double(:commit_1, sha: 'f00ba7', safe_message: "Initial commit\n\nCreate the app") }
+ let(:commit_2) { double(:commit_2, sha: 'f00ba7', safe_message: 'This is a bad commit message!') }
let(:commits) { nil }
let(:service) do
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 5d226f34d2d..44a83c436cb 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -28,6 +28,7 @@ describe MergeRequests::CreateService do
it 'creates an MR' do
expect(merge_request).to be_valid
+ expect(merge_request.work_in_progress?).to be(false)
expect(merge_request.title).to eq('Awesome merge_request')
expect(merge_request.assignee).to be_nil
expect(merge_request.merge_params['force_remove_source_branch']).to eq('1')
@@ -62,6 +63,40 @@ describe MergeRequests::CreateService do
expect(Event.where(attributes).count).to eq(1)
end
+ describe 'when marked with /wip' do
+ context 'in title and in description' do
+ let(:opts) do
+ {
+ title: 'WIP: Awesome merge_request',
+ description: "well this is not done yet\n/wip",
+ source_branch: 'feature',
+ target_branch: 'master',
+ assignee: assignee
+ }
+ end
+
+ it 'sets MR to WIP' do
+ expect(merge_request.work_in_progress?).to be(true)
+ end
+ end
+
+ context 'in description only' do
+ let(:opts) do
+ {
+ title: 'Awesome merge_request',
+ description: "well this is not done yet\n/wip",
+ source_branch: 'feature',
+ target_branch: 'master',
+ assignee: assignee
+ }
+ end
+
+ it 'sets MR to WIP' do
+ expect(merge_request.work_in_progress?).to be(true)
+ end
+ end
+ end
+
context 'when merge request is assigned to someone' do
let(:opts) do
{
diff --git a/spec/services/notes/create_service_spec.rb b/spec/services/notes/create_service_spec.rb
index 0ae26e87154..f5cff66de6d 100644
--- a/spec/services/notes/create_service_spec.rb
+++ b/spec/services/notes/create_service_spec.rb
@@ -57,32 +57,55 @@ describe Notes::CreateService do
end
end
- describe 'note with commands' do
- describe '/close, /label, /assign & /milestone' do
- let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) }
+ context 'note with commands' do
+ context 'as a user who can update the target' do
+ context '/close, /label, /assign & /milestone' do
+ let(:note_text) { %(HELLO\n/close\n/assign @#{user.username}\nWORLD) }
- it 'saves the note and does not alter the note text' do
- expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original
+ it 'saves the note and does not alter the note text' do
+ expect_any_instance_of(Issues::UpdateService).to receive(:execute).and_call_original
- note = described_class.new(project, user, opts.merge(note: note_text)).execute
+ note = described_class.new(project, user, opts.merge(note: note_text)).execute
- expect(note.note).to eq "HELLO\nWORLD"
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
+ end
+
+ context '/merge with sha option' do
+ let(:note_text) { %(HELLO\n/merge\nWORLD) }
+ let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') }
+
+ it 'saves the note and exectues merge command' do
+ note = described_class.new(project, user, params).execute
+
+ expect(note.note).to eq "HELLO\nWORLD"
+ end
end
end
- describe '/merge with sha option' do
- let(:note_text) { %(HELLO\n/merge\nWORLD) }
- let(:params) { opts.merge(note: note_text, merge_request_diff_head_sha: 'sha') }
+ context 'as a user who cannot update the target' do
+ let(:note_text) { "HELLO\n/todo\n/assign #{user.to_reference}\nWORLD" }
+ let(:note) { described_class.new(project, user, opts.merge(note: note_text)).execute }
- it 'saves the note and exectues merge command' do
- note = described_class.new(project, user, params).execute
+ before do
+ project.team.find_member(user.id).update!(access_level: Gitlab::Access::GUEST)
+ end
+
+ it 'applies commands the user can execute' do
+ expect { note }.to change { user.todos_pending_count }.from(0).to(1)
+ end
+
+ it 'does not apply commands the user cannot execute' do
+ expect { note }.not_to change { issue.assignees }
+ end
+ it 'saves the note' do
expect(note.note).to eq "HELLO\nWORLD"
end
end
end
- describe 'personal snippet note' do
+ context 'personal snippet note' do
subject { described_class.new(nil, user, params).execute }
let(:snippet) { create(:personal_snippet) }
@@ -103,7 +126,7 @@ describe Notes::CreateService do
end
end
- describe 'note with emoji only' do
+ context 'note with emoji only' do
it 'creates regular note' do
opts = {
note: ':smile: ',
diff --git a/spec/services/notes/quick_actions_service_spec.rb b/spec/services/notes/quick_actions_service_spec.rb
index 5eafe56c99d..b1e218821d2 100644
--- a/spec/services/notes/quick_actions_service_spec.rb
+++ b/spec/services/notes/quick_actions_service_spec.rb
@@ -165,31 +165,17 @@ describe Notes::QuickActionsService do
let(:note) { create(:note_on_issue, project: project) }
- context 'with no current_user' do
- it 'returns false' do
- expect(described_class.supported?(note, nil)).to be_falsy
- end
- end
-
- context 'when current_user cannot update the noteable' do
- it 'returns false' do
- user = create(:user)
-
- expect(described_class.supported?(note, user)).to be_falsy
- end
- end
-
- context 'when current_user can update the noteable' do
+ context 'with a note on an issue' do
it 'returns true' do
- expect(described_class.supported?(note, master)).to be_truthy
+ expect(described_class.supported?(note)).to be_truthy
end
+ end
- context 'with a note on a commit' do
- let(:note) { create(:note_on_commit, project: project) }
+ context 'with a note on a commit' do
+ let(:note) { create(:note_on_commit, project: project) }
- it 'returns false' do
- expect(described_class.supported?(note, nil)).to be_falsy
- end
+ it 'returns false' do
+ expect(described_class.supported?(note)).to be_falsy
end
end
end
@@ -201,7 +187,7 @@ describe Notes::QuickActionsService do
service = described_class.new(project, master)
note = create(:note_on_issue, project: project)
- expect(described_class).to receive(:supported?).with(note, master)
+ expect(described_class).to receive(:supported?).with(note)
service.supported?(note)
end
diff --git a/spec/services/projects/update_pages_service_spec.rb b/spec/services/projects/update_pages_service_spec.rb
index bfb86284d86..934106627a9 100644
--- a/spec/services/projects/update_pages_service_spec.rb
+++ b/spec/services/projects/update_pages_service_spec.rb
@@ -34,6 +34,7 @@ describe Projects::UpdatePagesService do
context 'with expiry date' do
before do
build.artifacts_expire_in = "2 days"
+ build.save!
end
it "doesn't delete artifacts" do
@@ -105,6 +106,7 @@ describe Projects::UpdatePagesService do
context 'with expiry date' do
before do
build.artifacts_expire_in = "2 days"
+ build.save!
end
it "doesn't delete artifacts" do
@@ -159,6 +161,20 @@ describe Projects::UpdatePagesService do
expect(execute).not_to eq(:success)
end
+
+ context 'when timeout happens by DNS error' do
+ before do
+ allow_any_instance_of(described_class)
+ .to receive(:extract_zip_archive!).and_raise(SocketError)
+ end
+
+ it 'raises an error' do
+ expect { execute }.to raise_error(SocketError)
+
+ build.reload
+ expect(build.artifacts?).to eq(true)
+ end
+ end
end
end
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index a0b97ceead9..ad5a289290c 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -123,6 +123,40 @@ describe Projects::UpdateService do
end
end
+ context 'when we update project but not enabling a wiki' do
+ it 'does not try to create an empty wiki' do
+ FileUtils.rm_rf(project.wiki.repository.path)
+
+ result = update_project(project, user, { name: 'test1' })
+
+ expect(result).to eq({ status: :success })
+ expect(project.wiki_repository_exists?).to be false
+ end
+ end
+
+ context 'when enabling a wiki' do
+ it 'creates a wiki' do
+ project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED)
+ FileUtils.rm_rf(project.wiki.repository.path)
+
+ result = update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED })
+
+ expect(result).to eq({ status: :success })
+ expect(project.wiki_repository_exists?).to be true
+ expect(project.wiki_enabled?).to be true
+ end
+
+ it 'logs an error and creates a metric when wiki can not be created' do
+ project.project_feature.update(wiki_access_level: ProjectFeature::DISABLED)
+
+ expect_any_instance_of(ProjectWiki).to receive(:wiki).and_raise(ProjectWiki::CouldNotCreateWikiError)
+ expect_any_instance_of(described_class).to receive(:log_error).with("Could not create wiki for #{project.full_name}")
+ expect(Gitlab::Metrics).to receive(:counter)
+
+ update_project(project, user, project_feature_attributes: { wiki_access_level: ProjectFeature::ENABLED })
+ end
+ end
+
context 'when updating a project that contains container images' do
before do
stub_container_registry_config(enabled: true)
diff --git a/spec/services/system_hooks_service_spec.rb b/spec/services/system_hooks_service_spec.rb
index c40cd5b7548..51396d34f8f 100644
--- a/spec/services/system_hooks_service_spec.rb
+++ b/spec/services/system_hooks_service_spec.rb
@@ -30,6 +30,7 @@ describe SystemHooksService do
:old_path_with_namespace
)
end
+
it do
project.old_path_with_namespace = 'transfered_from_path'
expect(event_data(project, :transfer)).to include(
@@ -45,18 +46,21 @@ describe SystemHooksService do
:owner_name, :owner_email
)
end
+
it do
expect(event_data(group, :destroy)).to include(
:event_name, :name, :created_at, :updated_at, :path, :group_id,
:owner_name, :owner_email
)
end
+
it do
expect(event_data(group_member, :create)).to include(
:event_name, :created_at, :updated_at, :group_name, :group_path,
:group_id, :user_id, :user_username, :user_name, :user_email, :group_access
)
end
+
it do
expect(event_data(group_member, :destroy)).to include(
:event_name, :created_at, :updated_at, :group_name, :group_path,
@@ -70,6 +74,14 @@ describe SystemHooksService do
expect(data[:project_visibility]).to eq('private')
end
+ it 'handles nil datetime columns' do
+ user.update_attributes(created_at: nil, updated_at: nil)
+ data = event_data(user, :destroy)
+
+ expect(data[:created_at]).to be(nil)
+ expect(data[:updated_at]).to be(nil)
+ end
+
context 'group_rename' do
it 'contains old and new path' do
allow(group).to receive(:path_was).and_return('old-path')
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 5b5edc1aa0d..a3893188c6e 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -789,7 +789,7 @@ describe SystemNoteService do
object: {
url: project_commit_url(project, commit),
title: "GitLab: Mentioned on commit - #{commit.title}",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: false }
}
)
@@ -815,7 +815,7 @@ describe SystemNoteService do
object: {
url: project_issue_url(project, issue),
title: "GitLab: Mentioned on issue - #{issue.title}",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: false }
}
)
@@ -841,7 +841,7 @@ describe SystemNoteService do
object: {
url: project_snippet_url(project, snippet),
title: "GitLab: Mentioned on snippet - #{snippet.title}",
- icon: { title: "GitLab", url16x16: "https://gitlab.com/favicon.ico" },
+ icon: { title: "GitLab", url16x16: "http://localhost/favicon.ico" },
status: { resolved: false }
}
)
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index c0f3366fb52..9f6f0204a16 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -186,6 +186,10 @@ RSpec.configure do |config|
example.run if Gitlab::Database.postgresql?
end
+ config.around(:each, :mysql) do |example|
+ example.run if Gitlab::Database.mysql?
+ end
+
# This makes sure the `ApplicationController#can?` method is stubbed with the
# original implementation for all view specs.
config.before(:each, type: :view) do
diff --git a/spec/support/bare_repo_operations.rb b/spec/support/bare_repo_operations.rb
index 38d11992dc2..8eeaa37d3c5 100644
--- a/spec/support/bare_repo_operations.rb
+++ b/spec/support/bare_repo_operations.rb
@@ -11,6 +11,14 @@ class BareRepoOperations
@path_to_repo = path_to_repo
end
+ def commit_tree(tree_id, msg, parent: EMPTY_TREE_ID)
+ commit_tree_args = ['commit-tree', tree_id, '-m', msg]
+ commit_tree_args += ['-p', parent] unless parent == EMPTY_TREE_ID
+ commit_id = execute(commit_tree_args)
+
+ commit_id[0]
+ end
+
# Based on https://stackoverflow.com/a/25556917/1856239
def commit_file(file, dst_path, branch = 'master')
head_id = execute(['show', '--format=format:%H', '--no-patch', branch], allow_failure: true)[0] || EMPTY_TREE_ID
@@ -26,11 +34,9 @@ class BareRepoOperations
tree_id = execute(['write-tree'])
- commit_tree_args = ['commit-tree', tree_id[0], '-m', "Add #{dst_path}"]
- commit_tree_args += ['-p', head_id] unless head_id == EMPTY_TREE_ID
- commit_id = execute(commit_tree_args)
+ commit_id = commit_tree(tree_id[0], "Add #{dst_path}", parent: head_id)
- execute(['update-ref', "refs/heads/#{branch}", commit_id[0]])
+ execute(['update-ref', "refs/heads/#{branch}", commit_id])
end
private
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 5189c57b7db..8603b7f3e2c 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -78,8 +78,10 @@ RSpec.configure do |config|
end
config.after(:example, :js) do |example|
- # prevent localstorage from introducing side effects based on test order
- execute_script("localStorage.clear();")
+ # prevent localStorage from introducing side effects based on test order
+ unless ['', 'about:blank', 'data:,'].include? Capybara.current_session.driver.browser.current_url
+ execute_script("localStorage.clear();")
+ end
# capybara/rspec already calls Capybara.reset_sessions! in an `after` hook,
# but `block_and_wait_for_requests_complete` is called before it so by
diff --git a/spec/support/cluster_application_spec.rb b/spec/support/cluster_application_spec.rb
deleted file mode 100644
index ab77910a050..00000000000
--- a/spec/support/cluster_application_spec.rb
+++ /dev/null
@@ -1,105 +0,0 @@
-shared_examples 'cluster application specs' do
- let(:factory_name) { described_class.to_s.downcase.gsub("::", "_") }
-
- describe '#name' do
- it 'is .application_name' do
- expect(subject.name).to eq(described_class.application_name)
- end
-
- it 'is recorded in Clusters::Cluster::APPLICATIONS' do
- expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
- end
- end
-
- describe '#status' do
- let(:cluster) { create(:cluster, :provided_by_gcp) }
-
- subject { described_class.new(cluster: cluster) }
-
- it 'defaults to :not_installable' do
- expect(subject.status_name).to be(:not_installable)
- end
-
- context 'when application helm is scheduled' do
- before do
- create(factory_name, :scheduled, cluster: cluster)
- end
-
- it 'defaults to :not_installable' do
- expect(subject.status_name).to be(:not_installable)
- end
- end
-
- context 'when application helm is installed' do
- before do
- create(:clusters_applications_helm, :installed, cluster: cluster)
- end
-
- it 'defaults to :installable' do
- expect(subject.status_name).to be(:installable)
- end
- end
- end
-
- describe '#install_command' do
- it 'has all the needed information' do
- expect(subject.install_command).to have_attributes(name: subject.name, install_helm: false)
- end
- end
-
- describe 'status state machine' do
- describe '#make_installing' do
- subject { create(factory_name, :scheduled) }
-
- it 'is installing' do
- subject.make_installing!
-
- expect(subject).to be_installing
- end
- end
-
- describe '#make_installed' do
- subject { create(factory_name, :installing) }
-
- it 'is installed' do
- subject.make_installed
-
- expect(subject).to be_installed
- end
- end
-
- describe '#make_errored' do
- subject { create(factory_name, :installing) }
- let(:reason) { 'some errors' }
-
- it 'is errored' do
- subject.make_errored(reason)
-
- expect(subject).to be_errored
- expect(subject.status_reason).to eq(reason)
- end
- end
-
- describe '#make_scheduled' do
- subject { create(factory_name, :installable) }
-
- it 'is scheduled' do
- subject.make_scheduled
-
- expect(subject).to be_scheduled
- end
-
- describe 'when was errored' do
- subject { create(factory_name, :errored) }
-
- it 'clears #status_reason' do
- expect(subject.status_reason).not_to be_nil
-
- subject.make_scheduled!
-
- expect(subject.status_reason).to be_nil
- end
- end
- end
- end
-end
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index d5ef80cfab2..73cc64c0b74 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -26,7 +26,19 @@ module CycleAnalyticsHelpers
ref: 'refs/heads/master').execute
end
- def create_merge_request_closing_issue(issue, message: nil, source_branch: nil, commit_message: 'commit message')
+ def create_cycle(user, project, issue, mr, milestone, pipeline)
+ issue.update(milestone: milestone)
+ pipeline.run
+
+ ci_build = create(:ci_build, pipeline: pipeline, status: :success, author: user)
+
+ merge_merge_requests_closing_issue(user, project, issue)
+ ProcessCommitWorker.new.perform(project.id, user.id, mr.commits.last.to_hash)
+
+ ci_build
+ end
+
+ def create_merge_request_closing_issue(user, project, issue, message: nil, source_branch: nil, commit_message: 'commit message')
if !source_branch || project.repository.commit(source_branch).blank?
source_branch = generate(:branch)
project.repository.add_branch(user, source_branch, 'master')
@@ -52,19 +64,19 @@ module CycleAnalyticsHelpers
mr
end
- def merge_merge_requests_closing_issue(issue)
+ def merge_merge_requests_closing_issue(user, project, issue)
merge_requests = issue.closed_by_merge_requests(user)
merge_requests.each { |merge_request| MergeRequests::MergeService.new(project, user).execute(merge_request) }
end
- def deploy_master(environment: 'production')
+ def deploy_master(user, project, environment: 'production')
dummy_job =
case environment
when 'production'
- dummy_production_job
+ dummy_production_job(user, project)
when 'staging'
- dummy_staging_job
+ dummy_staging_job(user, project)
else
raise ArgumentError
end
@@ -72,25 +84,24 @@ module CycleAnalyticsHelpers
CreateDeploymentService.new(dummy_job).execute
end
- def dummy_production_job
- @dummy_job ||= new_dummy_job('production')
+ def dummy_production_job(user, project)
+ new_dummy_job(user, project, 'production')
end
- def dummy_staging_job
- @dummy_job ||= new_dummy_job('staging')
+ def dummy_staging_job(user, project)
+ new_dummy_job(user, project, 'staging')
end
- def dummy_pipeline
- @dummy_pipeline ||=
- Ci::Pipeline.new(
- sha: project.repository.commit('master').sha,
- ref: 'master',
- source: :push,
- project: project,
- protected: false)
+ def dummy_pipeline(project)
+ Ci::Pipeline.new(
+ sha: project.repository.commit('master').sha,
+ ref: 'master',
+ source: :push,
+ project: project,
+ protected: false)
end
- def new_dummy_job(environment)
+ def new_dummy_job(user, project, environment)
project.environments.find_or_create_by(name: environment)
Ci::Build.new(
@@ -101,7 +112,7 @@ module CycleAnalyticsHelpers
tag: false,
name: 'dummy',
stage: 'dummy',
- pipeline: dummy_pipeline,
+ pipeline: dummy_pipeline(project),
protected: false)
end
diff --git a/spec/support/db_cleaner.rb b/spec/support/db_cleaner.rb
index 1809ae1d141..5edc5de2a09 100644
--- a/spec/support/db_cleaner.rb
+++ b/spec/support/db_cleaner.rb
@@ -28,11 +28,11 @@ RSpec.configure do |config|
end
config.before(:each, :js) do
- DatabaseCleaner.strategy = :deletion
+ DatabaseCleaner.strategy = :deletion, { cache_tables: false }
end
config.before(:each, :delete) do
- DatabaseCleaner.strategy = :deletion
+ DatabaseCleaner.strategy = :deletion, { cache_tables: false }
end
config.before(:each, :migration) do
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 2c20821ac3f..f61469f673d 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -127,7 +127,6 @@ shared_examples 'issuable record that supports quick actions in its description
it "does not close the #{issuable_type}" do
write_note("/close")
- expect(page).to have_content '/close'
expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_open
@@ -165,7 +164,6 @@ shared_examples 'issuable record that supports quick actions in its description
it "does not reopen the #{issuable_type}" do
write_note("/reopen")
- expect(page).to have_content '/reopen'
expect(page).not_to have_content 'Commands applied'
expect(issuable).to be_closed
@@ -195,10 +193,9 @@ shared_examples 'issuable record that supports quick actions in its description
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
- it "does not reopen the #{issuable_type}" do
+ it "does not change the #{issuable_type} title" do
write_note("/title Awesome new title")
- expect(page).to have_content '/title'
expect(page).not_to have_content 'Commands applied'
expect(issuable.reload.title).not_to eq 'Awesome new title'
diff --git a/spec/support/features/variable_list_shared_examples.rb b/spec/support/features/variable_list_shared_examples.rb
index 0d8f7a7aae6..f7f851eb1eb 100644
--- a/spec/support/features/variable_list_shared_examples.rb
+++ b/spec/support/features/variable_list_shared_examples.rb
@@ -261,6 +261,8 @@ shared_examples 'variable list' do
click_button('Save variables')
wait_for_requests
+ expect(all('.js-ci-variable-list-section .js-ci-variable-error-box ul li').count).to eq(1)
+
# We check the first row because it re-sorts to alphabetical order on refresh
page.within('.js-ci-variable-list-section') do
expect(find('.js-ci-variable-error-box')).to have_content(/Validation failed Variables have duplicate values \(.+\)/)
diff --git a/spec/support/gitlab-git-test.git/packed-refs b/spec/support/gitlab-git-test.git/packed-refs
index 507e4ce785a..ea50e4ad3f6 100644
--- a/spec/support/gitlab-git-test.git/packed-refs
+++ b/spec/support/gitlab-git-test.git/packed-refs
@@ -1,4 +1,4 @@
-# pack-refs with: peeled fully-peeled
+# pack-refs with: peeled fully-peeled sorted
0b4bc9a49b562e85de7cc9e834518ea6828729b9 refs/heads/feature
12d65c8dd2b2676fa3ac47d955accc085a37a9c1 refs/heads/fix
6473c90867124755509e100d0d35ebdc85a0b6ae refs/heads/fix-blob-path
diff --git a/spec/support/gitlab_verify.rb b/spec/support/gitlab_verify.rb
new file mode 100644
index 00000000000..13e2e37624d
--- /dev/null
+++ b/spec/support/gitlab_verify.rb
@@ -0,0 +1,45 @@
+RSpec.shared_examples 'Gitlab::Verify::BatchVerifier subclass' do
+ describe 'batching' do
+ let(:first_batch) { objects[0].id..objects[0].id }
+ let(:second_batch) { objects[1].id..objects[1].id }
+ let(:third_batch) { objects[2].id..objects[2].id }
+
+ it 'iterates through objects in batches' do
+ expect(collect_ranges).to eq([first_batch, second_batch, third_batch])
+ end
+
+ it 'allows the starting ID to be specified' do
+ expect(collect_ranges(start: second_batch.first)).to eq([second_batch, third_batch])
+ end
+
+ it 'allows the finishing ID to be specified' do
+ expect(collect_ranges(finish: second_batch.last)).to eq([first_batch, second_batch])
+ end
+ end
+end
+
+module GitlabVerifyHelpers
+ def collect_ranges(args = {})
+ verifier = described_class.new(args.merge(batch_size: 1))
+
+ collect_results(verifier).map { |range, _| range }
+ end
+
+ def collect_failures
+ verifier = described_class.new(batch_size: 1)
+
+ out = {}
+
+ collect_results(verifier).map { |_, failures| out.merge!(failures) }
+
+ out
+ end
+
+ def collect_results(verifier)
+ out = []
+
+ verifier.run_batches { |*args| out << args }
+
+ out
+ end
+end
diff --git a/spec/support/ldap_helpers.rb b/spec/support/ldap_helpers.rb
index 28d39a32f02..081ce0ad7b7 100644
--- a/spec/support/ldap_helpers.rb
+++ b/spec/support/ldap_helpers.rb
@@ -1,13 +1,13 @@
module LdapHelpers
def ldap_adapter(provider = 'ldapmain', ldap = double(:ldap))
- ::Gitlab::LDAP::Adapter.new(provider, ldap)
+ ::Gitlab::Auth::LDAP::Adapter.new(provider, ldap)
end
def user_dn(uid)
"uid=#{uid},ou=users,dc=example,dc=com"
end
- # Accepts a hash of Gitlab::LDAP::Config keys and values.
+ # Accepts a hash of Gitlab::Auth::LDAP::Config keys and values.
#
# Example:
# stub_ldap_config(
@@ -15,21 +15,21 @@ module LdapHelpers
# admin_group: 'my-admin-group'
# )
def stub_ldap_config(messages)
- allow_any_instance_of(::Gitlab::LDAP::Config).to receive_messages(messages)
+ allow_any_instance_of(::Gitlab::Auth::LDAP::Config).to receive_messages(messages)
end
# Stub an LDAP person search and provide the return entry. Specify `nil` for
# `entry` to simulate when an LDAP person is not found
#
# Example:
- # adapter = ::Gitlab::LDAP::Adapter.new('ldapmain', double(:ldap))
+ # adapter = ::Gitlab::Auth::LDAP::Adapter.new('ldapmain', double(:ldap))
# ldap_user_entry = ldap_user_entry('john_doe')
#
# stub_ldap_person_find_by_uid('john_doe', ldap_user_entry, adapter)
def stub_ldap_person_find_by_uid(uid, entry, provider = 'ldapmain')
- return_value = ::Gitlab::LDAP::Person.new(entry, provider) if entry.present?
+ return_value = ::Gitlab::Auth::LDAP::Person.new(entry, provider) if entry.present?
- allow(::Gitlab::LDAP::Person)
+ allow(::Gitlab::Auth::LDAP::Person)
.to receive(:find_by_uid).with(uid, any_args).and_return(return_value)
end
diff --git a/spec/support/login_helpers.rb b/spec/support/login_helpers.rb
index b52b6a28c54..d08183846a0 100644
--- a/spec/support/login_helpers.rb
+++ b/spec/support/login_helpers.rb
@@ -138,7 +138,7 @@ module LoginHelpers
Rails.application.routes.draw do
post '/users/auth/saml' => 'omniauth_callbacks#saml'
end
- allow(Gitlab::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
+ allow(Gitlab::Auth::OAuth::Provider).to receive_messages(providers: [:saml], config_for: mock_saml_config)
stub_omniauth_setting(messages)
allow_any_instance_of(Object).to receive(:user_saml_omniauth_authorize_path).and_return('/users/auth/saml')
allow_any_instance_of(Object).to receive(:omniauth_authorize_path).with(:user, "saml").and_return('/users/auth/saml')
@@ -149,10 +149,10 @@ module LoginHelpers
end
def stub_basic_saml_config
- allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
+ allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', args: {} } })
end
def stub_saml_group_config(groups)
- allow(Gitlab::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
+ allow(Gitlab::Auth::Saml::Config).to receive_messages({ options: { name: 'saml', groups_attribute: 'groups', external_groups: groups, args: {} } })
end
end
diff --git a/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
new file mode 100644
index 00000000000..87d12a784ba
--- /dev/null
+++ b/spec/support/shared_examples/models/cluster_application_core_shared_examples.rb
@@ -0,0 +1,70 @@
+shared_examples 'cluster application core specs' do |application_name|
+ it { is_expected.to belong_to(:cluster) }
+ it { is_expected.to validate_presence_of(:cluster) }
+
+ describe '#name' do
+ it 'is .application_name' do
+ expect(subject.name).to eq(described_class.application_name)
+ end
+
+ it 'is recorded in Clusters::Cluster::APPLICATIONS' do
+ expect(Clusters::Cluster::APPLICATIONS[subject.name]).to eq(described_class)
+ end
+ end
+
+ describe 'status state machine' do
+ describe '#make_installing' do
+ subject { create(application_name, :scheduled) }
+
+ it 'is installing' do
+ subject.make_installing!
+
+ expect(subject).to be_installing
+ end
+ end
+
+ describe '#make_installed' do
+ subject { create(application_name, :installing) }
+
+ it 'is installed' do
+ subject.make_installed
+
+ expect(subject).to be_installed
+ end
+ end
+
+ describe '#make_errored' do
+ subject { create(application_name, :installing) }
+ let(:reason) { 'some errors' }
+
+ it 'is errored' do
+ subject.make_errored(reason)
+
+ expect(subject).to be_errored
+ expect(subject.status_reason).to eq(reason)
+ end
+ end
+
+ describe '#make_scheduled' do
+ subject { create(application_name, :installable) }
+
+ it 'is scheduled' do
+ subject.make_scheduled
+
+ expect(subject).to be_scheduled
+ end
+
+ describe 'when was errored' do
+ subject { create(application_name, :errored) }
+
+ it 'clears #status_reason' do
+ expect(subject.status_reason).not_to be_nil
+
+ subject.make_scheduled!
+
+ expect(subject.status_reason).to be_nil
+ end
+ end
+ end
+ end
+end
diff --git a/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
new file mode 100644
index 00000000000..765dd32f4ba
--- /dev/null
+++ b/spec/support/shared_examples/models/cluster_application_status_shared_examples.rb
@@ -0,0 +1,31 @@
+shared_examples 'cluster application status specs' do |application_name|
+ describe '#status' do
+ let(:cluster) { create(:cluster, :provided_by_gcp) }
+
+ subject { described_class.new(cluster: cluster) }
+
+ it 'sets a default status' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+
+ context 'when application helm is scheduled' do
+ before do
+ create(:clusters_applications_helm, :scheduled, cluster: cluster)
+ end
+
+ it 'defaults to :not_installable' do
+ expect(subject.status_name).to be(:not_installable)
+ end
+ end
+
+ context 'when application is scheduled' do
+ before do
+ create(:clusters_applications_helm, :installed, cluster: cluster)
+ end
+
+ it 'sets a default status' do
+ expect(subject.status_name).to be(:installable)
+ end
+ end
+ end
+end
diff --git a/spec/support/slack_mattermost_notifications_shared_examples.rb b/spec/support/slack_mattermost_notifications_shared_examples.rb
index e827a8da0b7..5e1ce19eafb 100644
--- a/spec/support/slack_mattermost_notifications_shared_examples.rb
+++ b/spec/support/slack_mattermost_notifications_shared_examples.rb
@@ -337,6 +337,7 @@ RSpec.shared_examples 'slack or mattermost notifications' do
before do
chat_service.notify_only_default_branch = true
+ WebMock.stub_request(:post, webhook_url)
end
it 'does not call the Slack/Mattermost API for pipeline events' do
@@ -345,6 +346,23 @@ RSpec.shared_examples 'slack or mattermost notifications' do
expect(result).to be_falsy
end
+
+ it 'does not notify push events if they are not for the default branch' do
+ ref = "#{Gitlab::Git::BRANCH_REF_PREFIX}test"
+ push_sample_data = Gitlab::DataBuilder::Push.build(project, user, nil, nil, ref, [])
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).not_to have_requested(:post, webhook_url)
+ end
+
+ it 'notifies about push events for the default branch' do
+ push_sample_data = Gitlab::DataBuilder::Push.build_sample(project, user)
+
+ chat_service.execute(push_sample_data)
+
+ expect(WebMock).to have_requested(:post, webhook_url).once
+ end
end
context 'when disabled' do
diff --git a/spec/tasks/gitlab/check_rake_spec.rb b/spec/tasks/gitlab/check_rake_spec.rb
index 538ff952bf4..4eda618b6d6 100644
--- a/spec/tasks/gitlab/check_rake_spec.rb
+++ b/spec/tasks/gitlab/check_rake_spec.rb
@@ -11,8 +11,8 @@ describe 'gitlab:ldap:check rake task' do
context 'when LDAP is not enabled' do
it 'does not attempt to bind or search for users' do
- expect(Gitlab::LDAP::Config).not_to receive(:providers)
- expect(Gitlab::LDAP::Adapter).not_to receive(:open)
+ expect(Gitlab::Auth::LDAP::Config).not_to receive(:providers)
+ expect(Gitlab::Auth::LDAP::Adapter).not_to receive(:open)
run_rake_task('gitlab:ldap:check')
end
@@ -23,12 +23,12 @@ describe 'gitlab:ldap:check rake task' do
let(:adapter) { ldap_adapter('ldapmain', ldap) }
before do
- allow(Gitlab::LDAP::Config)
+ allow(Gitlab::Auth::LDAP::Config)
.to receive_messages(
enabled?: true,
providers: ['ldapmain']
)
- allow(Gitlab::LDAP::Adapter).to receive(:open).and_yield(adapter)
+ allow(Gitlab::Auth::LDAP::Adapter).to receive(:open).and_yield(adapter)
allow(adapter).to receive(:users).and_return([])
end
diff --git a/spec/tasks/gitlab/lfs/check_rake_spec.rb b/spec/tasks/gitlab/lfs/check_rake_spec.rb
new file mode 100644
index 00000000000..2610edf8bac
--- /dev/null
+++ b/spec/tasks/gitlab/lfs/check_rake_spec.rb
@@ -0,0 +1,28 @@
+require 'rake_helper'
+
+describe 'gitlab:lfs rake tasks' do
+ describe 'check' do
+ let!(:lfs_object) { create(:lfs_object, :with_file, :correct_oid) }
+
+ before do
+ Rake.application.rake_require('tasks/gitlab/lfs/check')
+ stub_env('VERBOSE' => 'true')
+ end
+
+ it 'outputs the integrity check for each batch' do
+ expect { run_rake_task('gitlab:lfs:check') }.to output(/Failures: 0/).to_stdout
+ end
+
+ it 'errors out about missing files on the file system' do
+ FileUtils.rm_f(lfs_object.file.path)
+
+ expect { run_rake_task('gitlab:lfs:check') }.to output(/No such file.*#{Regexp.quote(lfs_object.file.path)}/).to_stdout
+ end
+
+ it 'errors out about invalid checksum' do
+ File.truncate(lfs_object.file.path, 0)
+
+ expect { run_rake_task('gitlab:lfs:check') }.to output(/Checksum mismatch/).to_stdout
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/uploads_rake_spec.rb b/spec/tasks/gitlab/uploads/check_rake_spec.rb
index ac0005e51e0..5d597c66133 100644
--- a/spec/tasks/gitlab/uploads_rake_spec.rb
+++ b/spec/tasks/gitlab/uploads/check_rake_spec.rb
@@ -5,23 +5,24 @@ describe 'gitlab:uploads rake tasks' do
let!(:upload) { create(:upload, path: Rails.root.join('spec/fixtures/banana_sample.gif')) }
before do
- Rake.application.rake_require 'tasks/gitlab/uploads'
+ Rake.application.rake_require('tasks/gitlab/uploads/check')
+ stub_env('VERBOSE' => 'true')
end
- it 'outputs the integrity check for each uploaded file' do
- expect { run_rake_task('gitlab:uploads:check') }.to output(/Checking file \(#{upload.id}\): #{Regexp.quote(upload.absolute_path)}/).to_stdout
+ it 'outputs the integrity check for each batch' do
+ expect { run_rake_task('gitlab:uploads:check') }.to output(/Failures: 0/).to_stdout
end
it 'errors out about missing files on the file system' do
- create(:upload)
+ missing_upload = create(:upload)
- expect { run_rake_task('gitlab:uploads:check') }.to output(/File does not exist on the file system/).to_stdout
+ expect { run_rake_task('gitlab:uploads:check') }.to output(/No such file.*#{Regexp.quote(missing_upload.absolute_path)}/).to_stdout
end
it 'errors out about invalid checksum' do
upload.update_column(:checksum, '01a3156db2cf4f67ec823680b40b7302f89ab39179124ad219f94919b8a1769e')
- expect { run_rake_task('gitlab:uploads:check') }.to output(/File checksum \(9e697aa09fe196909813ee36103e34f721fe47a5fdc8aac0e4e4ac47b9b38282\) does not match the one in the database \(#{upload.checksum}\)/).to_stdout
+ expect { run_rake_task('gitlab:uploads:check') }.to output(/Checksum mismatch/).to_stdout
end
end
end
diff --git a/spec/validators/url_placeholder_validator_spec.rb b/spec/validators/url_placeholder_validator_spec.rb
new file mode 100644
index 00000000000..b76d8acdf88
--- /dev/null
+++ b/spec/validators/url_placeholder_validator_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe UrlPlaceholderValidator do
+ let(:validator) { described_class.new(attributes: [:link_url], **options) }
+ let!(:badge) { build(:badge) }
+ let(:placeholder_url) { 'http://www.example.com/%{project_path}/%{project_id}/%{default_branch}/%{commit_sha}' }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ describe '#validates_each' do
+ context 'with no options' do
+ let(:options) { {} }
+
+ it 'allows http and https protocols by default' do
+ expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
+ end
+
+ it 'checks that the url structure is valid' do
+ badge.link_url = placeholder_url
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'with placeholder regex' do
+ let(:options) { { placeholder_regex: /(project_path|project_id|commit_sha|default_branch)/ } }
+
+ it 'checks that the url is valid and obviate placeholders that match regex' do
+ badge.link_url = placeholder_url
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+ end
+ end
+end
diff --git a/spec/validators/url_validator_spec.rb b/spec/validators/url_validator_spec.rb
new file mode 100644
index 00000000000..763dff181d2
--- /dev/null
+++ b/spec/validators/url_validator_spec.rb
@@ -0,0 +1,46 @@
+require 'spec_helper'
+
+describe UrlValidator do
+ let(:validator) { described_class.new(attributes: [:link_url], **options) }
+ let!(:badge) { build(:badge) }
+
+ subject { validator.validate_each(badge, :link_url, badge.link_url) }
+
+ describe '#validates_each' do
+ context 'with no options' do
+ let(:options) { {} }
+
+ it 'allows http and https protocols by default' do
+ expect(validator.send(:default_options)[:protocols]).to eq %w(http https)
+ end
+
+ it 'checks that the url structure is valid' do
+ badge.link_url = 'http://www.google.es/%{whatever}'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+
+ context 'with protocols' do
+ let(:options) { { protocols: %w(http) } }
+
+ it 'allows urls with the defined protocols' do
+ badge.link_url = 'http://www.example.com'
+
+ subject
+
+ expect(badge.errors.empty?).to be true
+ end
+
+ it 'add error if the url protocol does not match the selected ones' do
+ badge.link_url = 'https://www.example.com'
+
+ subject
+
+ expect(badge.errors.empty?).to be false
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/_home_panel.html.haml_spec.rb b/spec/views/projects/_home_panel.html.haml_spec.rb
index 62af946dcab..15fce65979b 100644
--- a/spec/views/projects/_home_panel.html.haml_spec.rb
+++ b/spec/views/projects/_home_panel.html.haml_spec.rb
@@ -1,7 +1,8 @@
require 'spec_helper'
describe 'projects/_home_panel' do
- let(:project) { create(:project, :public) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, :public, namespace: group) }
let(:notification_settings) do
user&.notification_settings_for(project)
@@ -35,4 +36,55 @@ describe 'projects/_home_panel' do
expect(rendered).not_to have_selector('.notification_dropdown')
end
end
+
+ context 'when project' do
+ let!(:user) { create(:user) }
+ let(:badges) { project.badges }
+
+ context 'has no badges' do
+ it 'should not render any badge' do
+ render
+
+ expect(rendered).to have_selector('.project-badges')
+ expect(rendered).not_to have_selector('.project-badges > a')
+ end
+ end
+
+ shared_examples 'show badges' do
+ it 'should render the all badges' do
+ render
+
+ expect(rendered).to have_selector('.project-badges a')
+
+ badges.each do |badge|
+ expect(rendered).to have_link(href: badge.rendered_link_url)
+ end
+ end
+ end
+
+ context 'only has group badges' do
+ before do
+ create(:group_badge, group: project.group)
+ end
+
+ it_behaves_like 'show badges'
+ end
+
+ context 'only has project badges' do
+ before do
+ create(:project_badge, project: project)
+ end
+
+ it_behaves_like 'show badges'
+ end
+
+ context 'has both group and project badges' do
+ before do
+ create(:project_badge, project: project)
+ create(:group_badge, group: project.group)
+ end
+
+ it_behaves_like 'show badges'
+ end
+ end
end
diff --git a/spec/workers/authorized_projects_worker_spec.rb b/spec/workers/authorized_projects_worker_spec.rb
index 0d6eb536c33..d095138f6b7 100644
--- a/spec/workers/authorized_projects_worker_spec.rb
+++ b/spec/workers/authorized_projects_worker_spec.rb
@@ -1,79 +1,6 @@
require 'spec_helper'
describe AuthorizedProjectsWorker do
- let(:project) { create(:project) }
-
- def build_args_list(*ids, multiply: 1)
- args_list = ids.map { |id| [id] }
- args_list * multiply
- end
-
- describe '.bulk_perform_and_wait' do
- it 'schedules the ids and waits for the jobs to complete' do
- args_list = build_args_list(project.owner.id)
-
- project.owner.project_authorizations.delete_all
- described_class.bulk_perform_and_wait(args_list)
-
- expect(project.owner.project_authorizations.count).to eq(1)
- end
-
- it 'inlines workloads <= 3 jobs' do
- args_list = build_args_list(project.owner.id, multiply: 3)
- expect(described_class).to receive(:bulk_perform_inline).with(args_list)
-
- described_class.bulk_perform_and_wait(args_list)
- end
-
- it 'runs > 3 jobs using sidekiq' do
- project.owner.project_authorizations.delete_all
-
- expect(described_class).to receive(:bulk_perform_async).and_call_original
-
- args_list = build_args_list(project.owner.id, multiply: 4)
- described_class.bulk_perform_and_wait(args_list)
-
- expect(project.owner.project_authorizations.count).to eq(1)
- end
- end
-
- describe '.bulk_perform_inline' do
- it 'refreshes the authorizations inline' do
- project.owner.project_authorizations.delete_all
-
- expect_any_instance_of(described_class).to receive(:perform).and_call_original
-
- described_class.bulk_perform_inline(build_args_list(project.owner.id))
-
- expect(project.owner.project_authorizations.count).to eq(1)
- end
-
- it 'enqueues jobs if an error is raised' do
- invalid_id = -1
- args_list = build_args_list(project.owner.id, invalid_id)
-
- allow_any_instance_of(described_class).to receive(:perform).with(project.owner.id)
- allow_any_instance_of(described_class).to receive(:perform).with(invalid_id).and_raise(ArgumentError)
- expect(described_class).to receive(:bulk_perform_async).with(build_args_list(invalid_id))
-
- described_class.bulk_perform_inline(args_list)
- end
- end
-
- describe '.bulk_perform_async' do
- it "uses it's respective sidekiq queue" do
- args_list = build_args_list(project.owner.id)
- push_bulk_args = {
- 'class' => described_class,
- 'args' => args_list
- }
-
- expect(Sidekiq::Client).to receive(:push_bulk).with(push_bulk_args).once
-
- described_class.bulk_perform_async(args_list)
- end
- end
-
describe '#perform' do
let(:user) { create(:user) }
@@ -85,12 +12,6 @@ describe AuthorizedProjectsWorker do
job.perform(user.id)
end
- it 'notifies the JobWaiter when done if the key is provided' do
- expect(Gitlab::JobWaiter).to receive(:notify).with('notify-key', job.jid)
-
- job.perform(user.id, 'notify-key')
- end
-
context "when the user is not found" do
it "does nothing" do
expect_any_instance_of(User).not_to receive(:refresh_authorized_projects)
diff --git a/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
new file mode 100644
index 00000000000..2e2e9afd25a
--- /dev/null
+++ b/spec/workers/cluster_wait_for_ingress_ip_address_worker_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe ClusterWaitForIngressIpAddressWorker do
+ describe '#perform' do
+ let(:service) { instance_double(Clusters::Applications::CheckIngressIpAddressService, execute: true) }
+ let(:application) { instance_double(Clusters::Applications::Ingress) }
+ let(:worker) { described_class.new }
+
+ before do
+ allow(worker)
+ .to receive(:find_application)
+ .with('ingress', 117)
+ .and_yield(application)
+
+ allow(Clusters::Applications::CheckIngressIpAddressService)
+ .to receive(:new)
+ .with(application)
+ .and_return(service)
+
+ allow(described_class)
+ .to receive(:perform_in)
+ end
+
+ it 'finds the application and calls CheckIngressIpAddressService#execute' do
+ worker.perform('ingress', 117)
+
+ expect(service).to have_received(:execute)
+ end
+ end
+end
diff --git a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
index 68cfe9d5545..615462380e0 100644
--- a/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
+++ b/spec/workers/concerns/gitlab/github_import/object_importer_spec.rb
@@ -25,7 +25,7 @@ describe Gitlab::GithubImport::ObjectImporter do
importer_class = double(:importer_class)
importer_instance = double(:importer_instance)
representation = double(:representation)
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
expect(worker)
diff --git a/spec/workers/concerns/waitable_worker_spec.rb b/spec/workers/concerns/waitable_worker_spec.rb
new file mode 100644
index 00000000000..4af0de86ac9
--- /dev/null
+++ b/spec/workers/concerns/waitable_worker_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+describe WaitableWorker do
+ let(:worker) do
+ Class.new do
+ def self.name
+ 'Gitlab::Foo::Bar::DummyWorker'
+ end
+
+ class << self
+ cattr_accessor(:counter) { 0 }
+ end
+
+ include ApplicationWorker
+ prepend WaitableWorker
+
+ def perform(i = 0)
+ self.class.counter += i
+ end
+ end
+ end
+
+ subject(:job) { worker.new }
+
+ describe '.bulk_perform_and_wait' do
+ it 'schedules the jobs and waits for them to complete' do
+ worker.bulk_perform_and_wait([[1], [2]])
+
+ expect(worker.counter).to eq(3)
+ end
+
+ it 'inlines workloads <= 3 jobs' do
+ args_list = [[1], [2], [3]]
+ expect(worker).to receive(:bulk_perform_inline).with(args_list).and_call_original
+
+ worker.bulk_perform_and_wait(args_list)
+
+ expect(worker.counter).to eq(6)
+ end
+
+ it 'runs > 3 jobs using sidekiq' do
+ expect(worker).to receive(:bulk_perform_async)
+
+ worker.bulk_perform_and_wait([[1], [2], [3], [4]])
+ end
+ end
+
+ describe '.bulk_perform_inline' do
+ it 'runs the jobs inline' do
+ expect(worker).not_to receive(:bulk_perform_async)
+
+ worker.bulk_perform_inline([[1], [2]])
+
+ expect(worker.counter).to eq(3)
+ end
+
+ it 'enqueues jobs if an error is raised' do
+ expect(worker).to receive(:bulk_perform_async).with([['foo']])
+
+ worker.bulk_perform_inline([[1], ['foo']])
+ end
+ end
+
+ describe '#perform' do
+ shared_examples 'perform' do
+ it 'notifies the JobWaiter when done if the key is provided' do
+ key = Gitlab::JobWaiter.new.key
+ expect(Gitlab::JobWaiter).to receive(:notify).with(key, job.jid)
+
+ job.perform(*args, key)
+ end
+
+ it 'does not notify the JobWaiter when done if no key is provided' do
+ expect(Gitlab::JobWaiter).not_to receive(:notify)
+
+ job.perform(*args)
+ end
+ end
+
+ context 'when the worker takes arguments' do
+ let(:args) { [1] }
+
+ it_behaves_like 'perform'
+ end
+
+ context 'when the worker takes no arguments' do
+ let(:args) { [] }
+
+ it_behaves_like 'perform'
+ end
+ end
+end
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index 47297de738b..74539a7e493 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -195,6 +195,12 @@ describe GitGarbageCollectWorker do
expect(File.exist?(bitmap_path(after_packs.first))).to eq(bitmaps_enabled)
end
+
+ it 'cleans up repository after finishing' do
+ expect_any_instance_of(Project).to receive(:cleanup).and_call_original
+
+ subject.perform(project.id, 'gc', lease_key, lease_uuid)
+ end
end
context 'with bitmaps enabled' do
diff --git a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
index 7c8c665a9b3..48e7eaf32fc 100644
--- a/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_diff_note_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportDiffNoteWorker do
describe '#import' do
it 'imports a diff note' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
index 4116380ff4d..8cf6ac15919 100644
--- a/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_issue_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportIssueWorker do
describe '#import' do
it 'imports an issue' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_note_worker_spec.rb b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
index 0ca825a722b..677697c02df 100644
--- a/spec/workers/gitlab/github_import/import_note_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_note_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportNoteWorker do
describe '#import' do
it 'imports a note' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
index d49f560af42..e287ddbe0d7 100644
--- a/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
+++ b/spec/workers/gitlab/github_import/import_pull_request_worker_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::GithubImport::ImportPullRequestWorker do
describe '#import' do
it 'imports a pull request' do
- project = double(:project, path_with_namespace: 'foo/bar')
+ project = double(:project, full_path: 'foo/bar')
client = double(:client)
importer = double(:importer)
hash = {
diff --git a/spec/workers/namespaceless_project_destroy_worker_spec.rb b/spec/workers/namespaceless_project_destroy_worker_spec.rb
index ed8cedc0079..479d9396eca 100644
--- a/spec/workers/namespaceless_project_destroy_worker_spec.rb
+++ b/spec/workers/namespaceless_project_destroy_worker_spec.rb
@@ -22,7 +22,9 @@ describe NamespacelessProjectDestroyWorker do
end
end
- context 'project has no namespace' do
+ # Only possible with schema 20180222043024 and lower.
+ # Project#namespace_id has not null constraint since then
+ context 'project has no namespace', :migration, schema: 20180222043024 do
let!(:project) do
project = build(:project, namespace_id: nil)
project.save(validate: false)
diff --git a/spec/workers/plugin_worker_spec.rb b/spec/workers/plugin_worker_spec.rb
new file mode 100644
index 00000000000..9238a8199bc
--- /dev/null
+++ b/spec/workers/plugin_worker_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe PluginWorker do
+ include RepoHelpers
+
+ let(:filename) { 'my_plugin.rb' }
+ let(:data) { { 'event_name' => 'project_create' } }
+
+ subject { described_class.new }
+
+ describe '#perform' do
+ it 'executes Gitlab::Plugin with expected values' do
+ allow(Gitlab::Plugin).to receive(:execute).with(filename, data).and_return([true, ''])
+
+ expect(subject.perform(filename, data)).to be_truthy
+ end
+
+ it 'logs message in case of plugin execution failure' do
+ allow(Gitlab::Plugin).to receive(:execute).with(filename, data).and_return([false, 'permission denied'])
+
+ expect(Gitlab::PluginLogger).to receive(:error)
+ expect(subject.perform(filename, data)).to be_truthy
+ end
+ end
+end
diff --git a/spec/workers/process_commit_worker_spec.rb b/spec/workers/process_commit_worker_spec.rb
index 76ef57b6b1e..ac79d9c0ac1 100644
--- a/spec/workers/process_commit_worker_spec.rb
+++ b/spec/workers/process_commit_worker_spec.rb
@@ -20,32 +20,6 @@ describe ProcessCommitWorker do
worker.perform(project.id, -1, commit.to_hash)
end
- context 'when commit is a merge request merge commit' do
- let(:merge_request) do
- create(:merge_request,
- description: "Closes #{issue.to_reference}",
- source_branch: 'feature-merged',
- target_branch: 'master',
- source_project: project)
- end
-
- let(:commit) do
- project.repository.create_branch('feature-merged', 'feature')
-
- sha = project.repository.merge(user,
- merge_request.diff_head_sha,
- merge_request,
- "Closes #{issue.to_reference}")
- project.repository.commit(sha)
- end
-
- it 'it does not close any issues from the commit message' do
- expect(worker).not_to receive(:close_issues)
-
- worker.perform(project.id, user.id, commit.to_hash)
- end
- end
-
it 'processes the commit message' do
expect(worker).to receive(:process_commit_message).and_call_original
@@ -73,13 +47,21 @@ describe ProcessCommitWorker do
describe '#process_commit_message' do
context 'when pushing to the default branch' do
- it 'closes issues that should be closed per the commit message' do
+ before do
allow(commit).to receive(:safe_message).and_return("Closes #{issue.to_reference}")
+ end
+ it 'closes issues that should be closed per the commit message' do
expect(worker).to receive(:close_issues).with(project, user, user, commit, [issue])
worker.process_commit_message(project, commit, user, user, true)
end
+
+ it 'creates cross references' do
+ expect(commit).to receive(:create_cross_references!).with(user, [issue])
+
+ worker.process_commit_message(project, commit, user, user, true)
+ end
end
context 'when pushing to a non-default branch' do
@@ -90,12 +72,44 @@ describe ProcessCommitWorker do
worker.process_commit_message(project, commit, user, user, false)
end
+
+ it 'does not create cross references' do
+ expect(commit).to receive(:create_cross_references!).with(user, [])
+
+ worker.process_commit_message(project, commit, user, user, false)
+ end
end
- it 'creates cross references' do
- expect(commit).to receive(:create_cross_references!)
+ context 'when commit is a merge request merge commit to the default branch' do
+ let(:merge_request) do
+ create(:merge_request,
+ description: "Closes #{issue.to_reference}",
+ source_branch: 'feature-merged',
+ target_branch: 'master',
+ source_project: project)
+ end
- worker.process_commit_message(project, commit, user, user)
+ let(:commit) do
+ project.repository.create_branch('feature-merged', 'feature')
+
+ MergeRequests::MergeService
+ .new(project, merge_request.author)
+ .execute(merge_request)
+
+ merge_request.reload.merge_commit
+ end
+
+ it 'does not close any issues from the commit message' do
+ expect(worker).not_to receive(:close_issues)
+
+ worker.process_commit_message(project, commit, user, user, true)
+ end
+
+ it 'still creates cross references' do
+ expect(commit).to receive(:create_cross_references!).with(user, [])
+
+ worker.process_commit_message(project, commit, user, user, true)
+ end
end
end
diff --git a/vendor/project_templates/express.tar.gz b/vendor/project_templates/express.tar.gz
index dcf5e4a0416..06093deb459 100644
--- a/vendor/project_templates/express.tar.gz
+++ b/vendor/project_templates/express.tar.gz
Binary files differ
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index d4856090ed9..85cc1b6bb78 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ
diff --git a/vendor/project_templates/spring.tar.gz b/vendor/project_templates/spring.tar.gz
index 6ee7e76f676..e98d3ce7b8f 100644
--- a/vendor/project_templates/spring.tar.gz
+++ b/vendor/project_templates/spring.tar.gz
Binary files differ
diff --git a/vendor/prometheus/values.yaml b/vendor/prometheus/values.yaml
index db967514be7..859f2ad82a4 100644
--- a/vendor/prometheus/values.yaml
+++ b/vendor/prometheus/values.yaml
@@ -10,6 +10,9 @@ nodeExporter:
pushgateway:
enabled: false
+rbac:
+ create: false
+
server:
image:
tag: v2.1.0
diff --git a/vendor/runner/values.yaml b/vendor/runner/values.yaml
new file mode 100644
index 00000000000..e5f95152ac7
--- /dev/null
+++ b/vendor/runner/values.yaml
@@ -0,0 +1,23 @@
+## Configure the maximum number of concurrent jobs
+## - Documentation: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
+## - Default value: 10
+## - Currently don't support auto-scaling.
+concurrent: 4
+
+## Defines in seconds how often to check GitLab for a new builds
+## - Documentation: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-global-section
+## - Default value: 3
+checkInterval: 3
+
+## For RBAC support
+rbac:
+ create: false
+ clusterWideAccess: false
+
+## Configuration for the Pods that that the runner launches for each new job
+runners:
+ image: ubuntu:16.04
+ builds: {}
+ services: {}
+ helpers: {}
+resources: {}
diff --git a/yarn.lock b/yarn.lock
index 4d7dc1be854..ab0ad265d81 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -8732,9 +8732,9 @@ webpack-dev-middleware@1.12.2, webpack-dev-middleware@^1.12.0:
range-parser "^1.0.3"
time-stamp "^2.0.0"
-webpack-dev-server@^2.11.1:
- version "2.11.1"
- resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.1.tgz#6f9358a002db8403f016e336816f4485384e5ec0"
+webpack-dev-server@^2.11.2:
+ version "2.11.2"
+ resolved "https://registry.yarnpkg.com/webpack-dev-server/-/webpack-dev-server-2.11.2.tgz#1f4f4c78bf1895378f376815910812daf79a216f"
dependencies:
ansi-html "0.0.7"
array-includes "^3.0.3"