summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.csscomb.json2
-rw-r--r--.flayignore1
-rw-r--r--.gitlab-ci.yml15
-rw-r--r--.rubocop.yml8
-rw-r--r--.rubocop_todo.yml170
-rw-r--r--.scss-lint.yml2
-rw-r--r--CHANGELOG156
-rw-r--r--CONTRIBUTING.md49
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile54
-rw-r--r--Gemfile.lock77
-rw-r--r--README.md6
-rw-r--r--app/assets/javascripts/LabelManager.js115
-rw-r--r--app/assets/javascripts/activities.js12
-rw-r--r--app/assets/javascripts/api.js12
-rw-r--r--app/assets/javascripts/application.js2
-rw-r--r--app/assets/javascripts/awards_handler.js2
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js46
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.es640
-rw-r--r--app/assets/javascripts/blob/blob_gitignore_selector.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selector.js2
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js25
-rw-r--r--app/assets/javascripts/blob/blob_license_selectors.js.es621
-rw-r--r--app/assets/javascripts/blob/template_selector.js100
-rw-r--r--app/assets/javascripts/blob/template_selector.js.es6102
-rw-r--r--app/assets/javascripts/blob_edit/edit_blob.js19
-rw-r--r--app/assets/javascripts/boards/components/board.js.es68
-rw-r--r--app/assets/javascripts/boards/components/board_blank_state.js.es66
-rw-r--r--app/assets/javascripts/boards/components/board_list.js.es69
-rw-r--r--app/assets/javascripts/boards/components/board_new_issue.js.es658
-rw-r--r--app/assets/javascripts/boards/components/new_list_dropdown.js.es63
-rw-r--r--app/assets/javascripts/boards/mixins/sortable_default_options.js.es62
-rw-r--r--app/assets/javascripts/boards/models/list.js.es611
-rw-r--r--app/assets/javascripts/boards/services/board_service.js.es66
-rw-r--r--app/assets/javascripts/build.js2
-rw-r--r--app/assets/javascripts/commit/image_file.js (renamed from app/assets/javascripts/commit/image-file.js)0
-rw-r--r--app/assets/javascripts/compare_autocomplete.js5
-rw-r--r--app/assets/javascripts/copy_to_clipboard.js18
-rw-r--r--app/assets/javascripts/create_label.js.es69
-rw-r--r--app/assets/javascripts/cycle_analytics.js.es6 (renamed from app/assets/javascripts/cycle-analytics.js.es6)0
-rw-r--r--app/assets/javascripts/diff.js7
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_btn.js.es68
-rw-r--r--app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es66
-rw-r--r--app/assets/javascripts/diff_notes/mixins/namespace.js.es69
-rw-r--r--app/assets/javascripts/diff_notes/services/resolve.js.es626
-rw-r--r--app/assets/javascripts/dispatcher.js19
-rw-r--r--app/assets/javascripts/gfm_auto_complete.js.es638
-rw-r--r--app/assets/javascripts/gl_dropdown.js14
-rw-r--r--app/assets/javascripts/groups_select.js5
-rw-r--r--app/assets/javascripts/issuable.js.es61
-rw-r--r--app/assets/javascripts/issues_bulk_assignment.js.es6 (renamed from app/assets/javascripts/issues-bulk-assignment.js)128
-rw-r--r--app/assets/javascripts/label_manager.js.es6106
-rw-r--r--app/assets/javascripts/labels_select.js145
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js5
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js2
-rw-r--r--app/assets/javascripts/merge_conflict_data_provider.js.es616
-rw-r--r--app/assets/javascripts/merge_conflict_resolver.js.es65
-rw-r--r--app/assets/javascripts/merge_request.js9
-rw-r--r--app/assets/javascripts/merge_request_tabs.js55
-rw-r--r--app/assets/javascripts/milestone_select.js25
-rw-r--r--app/assets/javascripts/network/branch_graph.js (renamed from app/assets/javascripts/network/branch-graph.js)0
-rw-r--r--app/assets/javascripts/notes.js11
-rw-r--r--app/assets/javascripts/pipeline.js.es645
-rw-r--r--app/assets/javascripts/profile/gl_crop.js.es6 (renamed from app/assets/javascripts/profile/gl_crop.js)123
-rw-r--r--app/assets/javascripts/profile/profile.js106
-rw-r--r--app/assets/javascripts/profile/profile.js.es6100
-rw-r--r--app/assets/javascripts/project_select.js4
-rw-r--r--app/assets/javascripts/search.js2
-rw-r--r--app/assets/javascripts/search_autocomplete.js.es6 (renamed from app/assets/javascripts/search_autocomplete.js)187
-rw-r--r--app/assets/javascripts/single_file_diff.js13
-rw-r--r--app/assets/javascripts/templates/issuable_template_selector.js.es622
-rw-r--r--app/assets/javascripts/templates/issuable_template_selectors.js.es612
-rw-r--r--app/assets/javascripts/todos.js.es6 (renamed from app/assets/javascripts/todos.js)143
-rw-r--r--app/assets/javascripts/user.js.es610
-rw-r--r--app/assets/javascripts/user_tabs.js188
-rw-r--r--app/assets/javascripts/user_tabs.js.es6157
-rw-r--r--app/assets/javascripts/users_select.js27
-rw-r--r--app/assets/stylesheets/framework/avatar.scss4
-rw-r--r--app/assets/stylesheets/framework/blocks.scss8
-rw-r--r--app/assets/stylesheets/framework/buttons.scss23
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss23
-rw-r--r--app/assets/stylesheets/framework/files.scss9
-rw-r--r--app/assets/stylesheets/framework/flash.scss5
-rw-r--r--app/assets/stylesheets/framework/forms.scss6
-rw-r--r--app/assets/stylesheets/framework/header.scss16
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss2
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/mixins.scss11
-rw-r--r--app/assets/stylesheets/framework/mobile.scss2
-rw-r--r--app/assets/stylesheets/framework/selects.scss23
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss7
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss5
-rw-r--r--app/assets/stylesheets/pages/boards.scss33
-rw-r--r--app/assets/stylesheets/pages/builds.scss13
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/editor.scss21
-rw-r--r--app/assets/stylesheets/pages/environments.scss33
-rw-r--r--app/assets/stylesheets/pages/events.scss2
-rw-r--r--app/assets/stylesheets/pages/groups.scss36
-rw-r--r--app/assets/stylesheets/pages/labels.scss11
-rw-r--r--app/assets/stylesheets/pages/login.scss6
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss21
-rw-r--r--app/assets/stylesheets/pages/milestone.scss1
-rw-r--r--app/assets/stylesheets/pages/notes.scss2
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss159
-rw-r--r--app/assets/stylesheets/pages/profile.scss25
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss4
-rw-r--r--app/assets/stylesheets/pages/projects.scss69
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/assets/stylesheets/pages/snippets.scss4
-rw-r--r--app/assets/stylesheets/pages/status.scss2
-rw-r--r--app/assets/stylesheets/pages/todos.scss19
-rw-r--r--app/assets/stylesheets/pages/tree.scss7
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/groups_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb2
-rw-r--r--app/controllers/application_controller.rb3
-rw-r--r--app/controllers/ci/application_controller.rb7
-rw-r--r--app/controllers/ci/lints_controller.rb3
-rw-r--r--app/controllers/ci/projects_controller.rb2
-rw-r--r--app/controllers/concerns/authenticates_with_two_factor.rb15
-rw-r--r--app/controllers/concerns/membership_actions.rb20
-rw-r--r--app/controllers/concerns/spammable_actions.rb2
-rw-r--r--app/controllers/explore/projects_controller.rb3
-rw-r--r--app/controllers/groups/group_members_controller.rb7
-rw-r--r--app/controllers/import/gitlab_projects_controller.rb5
-rw-r--r--app/controllers/jwt_controller.rb21
-rw-r--r--app/controllers/namespaces_controller.rb25
-rw-r--r--app/controllers/profiles_controller.rb3
-rw-r--r--app/controllers/projects/boards/issues_controller.rb39
-rw-r--r--app/controllers/projects/boards_controller.rb2
-rw-r--r--app/controllers/projects/commit_controller.rb17
-rw-r--r--app/controllers/projects/group_links_controller.rb24
-rw-r--r--app/controllers/projects/issues_controller.rb5
-rw-r--r--app/controllers/projects/labels_controller.rb10
-rw-r--r--app/controllers/projects/merge_requests_controller.rb83
-rw-r--r--app/controllers/projects/project_members_controller.rb8
-rw-r--r--app/controllers/projects_controller.rb11
-rw-r--r--app/controllers/users_controller.rb2
-rw-r--r--app/finders/access_requests_finder.rb27
-rw-r--r--app/finders/issuable_finder.rb15
-rw-r--r--app/finders/trending_projects_finder.rb11
-rw-r--r--app/helpers/appearances_helper.rb2
-rw-r--r--app/helpers/application_helper.rb26
-rw-r--r--app/helpers/application_settings_helper.rb12
-rw-r--r--app/helpers/avatars_helper.rb5
-rw-r--r--app/helpers/broadcast_messages_helper.rb6
-rw-r--r--app/helpers/button_helper.rb3
-rw-r--r--app/helpers/ci_status_helper.rb2
-rw-r--r--app/helpers/dropdowns_helper.rb3
-rw-r--r--app/helpers/gitlab_markdown_helper.rb44
-rw-r--r--app/helpers/issuables_helper.rb52
-rw-r--r--app/helpers/issues_helper.rb5
-rw-r--r--app/helpers/labels_helper.rb5
-rw-r--r--app/helpers/lfs_helper.rb8
-rw-r--r--app/helpers/milestones_helper.rb5
-rw-r--r--app/helpers/page_layout_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb2
-rw-r--r--app/helpers/search_helper.rb14
-rw-r--r--app/helpers/selects_helper.rb6
-rw-r--r--app/helpers/todos_helper.rb20
-rw-r--r--app/mailers/devise_mailer.rb8
-rw-r--r--app/mailers/emails/members.rb2
-rw-r--r--app/mailers/notify.rb3
-rw-r--r--app/models/abuse_report.rb7
-rw-r--r--app/models/appearance.rb4
-rw-r--r--app/models/application_setting.rb7
-rw-r--r--app/models/board.rb8
-rw-r--r--app/models/broadcast_message.rb3
-rw-r--r--app/models/ci/build.rb2
-rw-r--r--app/models/ci/pipeline.rb5
-rw-r--r--app/models/ci/runner.rb2
-rw-r--r--app/models/commit_range.rb2
-rw-r--r--app/models/commit_status.rb50
-rw-r--r--app/models/concerns/access_requestable.rb5
-rw-r--r--app/models/concerns/cache_markdown_field.rb131
-rw-r--r--app/models/concerns/has_status.rb28
-rw-r--r--app/models/concerns/issuable.rb4
-rw-r--r--app/models/concerns/mentionable.rb27
-rw-r--r--app/models/cycle_analytics/summary.rb28
-rw-r--r--app/models/deployment.rb12
-rw-r--r--app/models/environment.rb4
-rw-r--r--app/models/event.rb19
-rw-r--r--app/models/global_label.rb4
-rw-r--r--app/models/global_milestone.rb5
-rw-r--r--app/models/group.rb36
-rw-r--r--app/models/label.rb3
-rw-r--r--app/models/member.rb84
-rw-r--r--app/models/members/group_member.rb16
-rw-r--r--app/models/members/project_member.rb44
-rw-r--r--app/models/merge_request.rb44
-rw-r--r--app/models/merge_request_diff.rb14
-rw-r--r--app/models/milestone.rb10
-rw-r--r--app/models/namespace.rb17
-rw-r--r--app/models/note.rb5
-rw-r--r--app/models/project.rb23
-rw-r--r--app/models/project_feature.rb5
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb4
-rw-r--r--app/models/project_services/slack_service/issue_message.rb2
-rw-r--r--app/models/project_services/slack_service/merge_message.rb2
-rw-r--r--app/models/project_services/slack_service/note_message.rb2
-rw-r--r--app/models/project_services/slack_service/wiki_page_message.rb2
-rw-r--r--app/models/project_team.rb14
-rw-r--r--app/models/release.rb4
-rw-r--r--app/models/repository.rb56
-rw-r--r--app/models/service.rb1
-rw-r--r--app/models/snippet.rb10
-rw-r--r--app/models/trending_project.rb35
-rw-r--r--app/models/user.rb28
-rw-r--r--app/policies/project_policy.rb18
-rw-r--r--app/services/auth/container_registry_authentication_service.rb11
-rw-r--r--app/services/base_service.rb7
-rw-r--r--app/services/boards/issues/create_service.rb16
-rw-r--r--app/services/boards/issues/list_service.rb7
-rw-r--r--app/services/boards/lists/generate_service.rb6
-rw-r--r--app/services/ci/process_pipeline_service.rb2
-rw-r--r--app/services/files/base_service.rb11
-rw-r--r--app/services/files/multi_service.rb124
-rw-r--r--app/services/files/update_service.rb6
-rw-r--r--app/services/issuable_base_service.rb1
-rw-r--r--app/services/members/approve_access_request_service.rb31
-rw-r--r--app/services/members/authorized_destroy_service.rb2
-rw-r--r--app/services/members/destroy_service.rb39
-rw-r--r--app/services/members/request_access_service.rb25
-rw-r--r--app/services/merge_requests/base_service.rb9
-rw-r--r--app/services/merge_requests/build_service.rb23
-rw-r--r--app/services/merge_requests/post_merge_service.rb1
-rw-r--r--app/services/merge_requests/update_service.rb15
-rw-r--r--app/services/notification_service.rb19
-rw-r--r--app/services/projects/create_service.rb20
-rw-r--r--app/services/projects/fork_service.rb2
-rw-r--r--app/services/projects/import_service.rb5
-rw-r--r--app/services/slash_commands/interpret_service.rb16
-rw-r--r--app/services/system_hooks_service.rb2
-rw-r--r--app/services/system_note_service.rb24
-rw-r--r--app/validators/namespace_validator.rb6
-rw-r--r--app/views/admin/abuse_reports/_abuse_report.html.haml2
-rw-r--r--app/views/admin/background_jobs/_head.html.haml49
-rw-r--r--app/views/admin/broadcast_messages/_form.html.haml9
-rw-r--r--app/views/admin/broadcast_messages/preview.js.haml2
-rw-r--r--app/views/admin/dashboard/_head.html.haml57
-rw-r--r--app/views/admin/dashboard/index.html.haml5
-rw-r--r--app/views/admin/groups/_group.html.haml2
-rw-r--r--app/views/admin/labels/_form.html.haml2
-rw-r--r--app/views/admin/labels/_label.html.haml2
-rw-r--r--app/views/admin/projects/index.html.haml2
-rw-r--r--app/views/admin/runners/index.html.haml22
-rw-r--r--app/views/admin/runners/show.html.haml12
-rw-r--r--app/views/ci/lints/_create.html.haml12
-rw-r--r--app/views/dashboard/groups/_empty_state.html.haml7
-rw-r--r--app/views/dashboard/groups/index.html.haml13
-rw-r--r--app/views/dashboard/snippets/index.html.haml12
-rw-r--r--app/views/dashboard/todos/_todo.html.haml1
-rw-r--r--app/views/dashboard/todos/index.html.haml2
-rw-r--r--app/views/devise/confirmations/almost_there.haml4
-rw-r--r--app/views/devise/sessions/_new_ldap.html.haml2
-rw-r--r--app/views/discussions/_resolve_all.html.haml5
-rw-r--r--app/views/doorkeeper/authorized_applications/_delete_form.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml2
-rw-r--r--app/views/explore/projects/_filter.html.haml8
-rw-r--r--app/views/explore/snippets/index.html.haml5
-rw-r--r--app/views/groups/milestones/new.html.haml4
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/index.html.haml2
-rw-r--r--app/views/layouts/_flash.html.haml6
-rw-r--r--app/views/layouts/_page.html.haml6
-rw-r--r--app/views/layouts/_search.html.haml39
-rw-r--r--app/views/layouts/devise.html.haml4
-rw-r--r--app/views/layouts/header/_default.html.haml4
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/profiles/show.html.haml3
-rw-r--r--app/views/projects/_activity.html.haml19
-rw-r--r--app/views/projects/_home_panel.html.haml2
-rw-r--r--app/views/projects/_last_push.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml7
-rw-r--r--app/views/projects/boards/components/_board.html.haml41
-rw-r--r--app/views/projects/boards/components/_card.html.haml6
-rw-r--r--app/views/projects/branches/index.html.haml2
-rw-r--r--app/views/projects/builds/_sidebar.html.haml6
-rw-r--r--app/views/projects/builds/_table.html.haml2
-rw-r--r--app/views/projects/builds/index.html.haml2
-rw-r--r--app/views/projects/buttons/_download.html.haml4
-rw-r--r--app/views/projects/buttons/_dropdown.html.haml3
-rw-r--r--app/views/projects/buttons/_fork.html.haml4
-rw-r--r--app/views/projects/ci/builds/_build.html.haml65
-rw-r--r--app/views/projects/ci/builds/_build_pipeline.html.haml6
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml56
-rw-r--r--app/views/projects/commit/_builds.html.haml2
-rw-r--r--app/views/projects/commit/_ci_menu.html.haml5
-rw-r--r--app/views/projects/commit/_commit_box.html.haml8
-rw-r--r--app/views/projects/commit/_pipeline.html.haml79
-rw-r--r--app/views/projects/commit/_pipeline_stage.html.haml2
-rw-r--r--app/views/projects/commit/_pipeline_status_group.html.haml19
-rw-r--r--app/views/projects/commit/_pipelines_list.haml12
-rw-r--r--app/views/projects/commit/pipelines.html.haml7
-rw-r--r--app/views/projects/commits/_commit.html.haml2
-rw-r--r--app/views/projects/commits/_head.html.haml45
-rw-r--r--app/views/projects/commits/show.html.haml3
-rw-r--r--app/views/projects/compare/_form.html.haml17
-rw-r--r--app/views/projects/compare/_ref_dropdown.html.haml1
-rw-r--r--app/views/projects/compare/index.html.haml2
-rw-r--r--app/views/projects/deployments/_actions.haml8
-rw-r--r--app/views/projects/deployments/_deployment.html.haml10
-rw-r--r--app/views/projects/diffs/_content.html.haml4
-rw-r--r--app/views/projects/diffs/_diffs.html.haml5
-rw-r--r--app/views/projects/diffs/_file.html.haml4
-rw-r--r--app/views/projects/diffs/_file_header.html.haml1
-rw-r--r--app/views/projects/edit.html.haml3
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/_environment.html.haml15
-rw-r--r--app/views/projects/environments/index.html.haml46
-rw-r--r--app/views/projects/environments/show.html.haml43
-rw-r--r--app/views/projects/forks/index.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status.html.haml2
-rw-r--r--app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml6
-rw-r--r--app/views/projects/graphs/_head.html.haml35
-rw-r--r--app/views/projects/group_links/index.html.haml6
-rw-r--r--app/views/projects/issues/_head.html.haml57
-rw-r--r--app/views/projects/issues/edit.html.haml2
-rw-r--r--app/views/projects/issues/index.html.haml3
-rw-r--r--app/views/projects/issues/show.html.haml8
-rw-r--r--app/views/projects/labels/_form.html.haml2
-rw-r--r--app/views/projects/labels/_label.html.haml2
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml2
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml13
-rw-r--r--app/views/projects/merge_requests/_new_diffs.html.haml1
-rw-r--r--app/views/projects/merge_requests/_new_submit.html.haml62
-rw-r--r--app/views/projects/merge_requests/_show.html.haml7
-rw-r--r--app/views/projects/merge_requests/edit.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_mr_box.html.haml4
-rw-r--r--app/views/projects/merge_requests/show/_mr_title.html.haml2
-rw-r--r--app/views/projects/merge_requests/show/_versions.html.haml14
-rw-r--r--app/views/projects/merge_requests/widget/_heading.html.haml10
-rw-r--r--app/views/projects/merge_requests/widget/open/_accept.html.haml2
-rw-r--r--app/views/projects/milestones/show.html.haml4
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/new.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml10
-rw-r--r--app/views/projects/pipelines/_head.html.haml49
-rw-r--r--app/views/projects/pipelines/_info.html.haml4
-rw-r--r--app/views/projects/pipelines/index.html.haml16
-rw-r--r--app/views/projects/repositories/_feed.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml4
-rw-r--r--app/views/projects/runners/_runner.html.haml2
-rw-r--r--app/views/projects/runners/_shared_runners.html.haml20
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml12
-rw-r--r--app/views/projects/runners/index.html.haml12
-rw-r--r--app/views/projects/show.html.haml5
-rw-r--r--app/views/projects/snippets/_actions.html.haml10
-rw-r--r--app/views/projects/snippets/index.html.haml7
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/tags/index.html.haml2
-rw-r--r--app/views/projects/tags/show.html.haml2
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/projects/wikis/_form.html.haml2
-rw-r--r--app/views/projects/wikis/_nav.html.haml25
-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/_milestone.html.haml2
-rw-r--r--app/views/search/results/_note.html.haml2
-rw-r--r--app/views/shared/_event_filter.html.haml1
-rw-r--r--app/views/shared/_label_row.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml2
-rw-r--r--app/views/shared/_sort_dropdown.html.haml24
-rw-r--r--app/views/shared/_visibility_level.html.haml2
-rw-r--r--app/views/shared/_visibility_radios.html.haml2
-rw-r--r--app/views/shared/groups/_group.html.haml2
-rw-r--r--app/views/shared/icons/_icon_empty_groups.svg1
-rw-r--r--app/views/shared/icons/_icon_no_wrap.svg3
-rw-r--r--app/views/shared/icons/_icon_soft_wrap.svg3
-rw-r--r--app/views/shared/icons/_illustration_no_commits.svg1
-rw-r--r--app/views/shared/issuable/_filter.html.haml11
-rw-r--r--app/views/shared/issuable/_form.html.haml35
-rw-r--r--app/views/shared/issuable/_label_dropdown.html.haml20
-rw-r--r--app/views/shared/issuable/_milestone_dropdown.html.haml22
-rw-r--r--app/views/shared/issuable/_nav.html.haml20
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml17
-rw-r--r--app/views/shared/milestones/_labels_tab.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml3
-rw-r--r--app/views/shared/notifications/_button.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--app/views/shared/snippets/_blob.html.haml7
-rw-r--r--app/views/shared/snippets/_header.html.haml2
-rw-r--r--app/views/snippets/_actions.html.haml10
-rw-r--r--app/views/snippets/_snippets.html.haml4
-rw-r--r--app/views/users/show.html.haml12
-rw-r--r--app/workers/clear_database_cache_worker.rb23
-rw-r--r--app/workers/expire_build_artifacts_worker.rb11
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb11
-rw-r--r--app/workers/process_pipeline_worker.rb10
-rw-r--r--app/workers/trending_projects_worker.rb11
-rw-r--r--app/workers/update_pipeline_worker.rb10
-rw-r--r--config/application.rb21
-rw-r--r--config/gitlab.yml.example1
-rw-r--r--config/initializers/1_settings.rb5
-rw-r--r--config/initializers/7_redis.rb3
-rw-r--r--config/initializers/ar5_batching.rb41
-rw-r--r--config/initializers/ar_speed_up_migration_checking.rb18
-rw-r--r--config/initializers/attr_encrypted_no_db_connection.rb25
-rw-r--r--config/initializers/connection_fix.rb2
-rw-r--r--config/initializers/gitlab_shell_secret_token.rb2
-rw-r--r--config/initializers/postgresql_limit_fix.rb27
-rw-r--r--config/initializers/sentry.rb2
-rw-r--r--config/routes.rb888
-rw-r--r--config/routes/admin.rb102
-rw-r--r--config/routes/api.rb2
-rw-r--r--config/routes/ci.rb15
-rw-r--r--config/routes/dashboard.rb27
-rw-r--r--config/routes/development.rb13
-rw-r--r--config/routes/explore.rb16
-rw-r--r--config/routes/group.rb26
-rw-r--r--config/routes/help.rb4
-rw-r--r--config/routes/import.rb42
-rw-r--r--config/routes/profile.rb43
-rw-r--r--config/routes/project.rb465
-rw-r--r--config/routes/sherlock.rb12
-rw-r--r--config/routes/sidekiq.rb4
-rw-r--r--config/routes/snippets.rb8
-rw-r--r--config/routes/uploads.rb21
-rw-r--r--config/routes/user.rb37
-rw-r--r--db/fixtures/development/06_teams.rb2
-rw-r--r--db/fixtures/development/14_pipelines.rb2
-rw-r--r--db/migrate/20160829114652_add_markdown_cache_columns.rb38
-rw-r--r--db/migrate/20160926145521_add_organization_to_user.rb12
-rw-r--r--db/migrate/20161007133303_precalculate_trending_projects.rb38
-rw-r--r--db/schema.rb46
-rw-r--r--doc/README.md5
-rw-r--r--doc/administration/auth/ldap.md6
-rw-r--r--doc/administration/container_registry.md96
-rw-r--r--doc/administration/environment_variables.md20
-rw-r--r--doc/administration/housekeeping.md2
-rw-r--r--doc/administration/img/housekeeping_settings.pngbin19347 -> 27420 bytes
-rw-r--r--doc/api/README.md15
-rw-r--r--doc/api/boards.md251
-rw-r--r--doc/api/builds.md30
-rw-r--r--doc/api/ci/runners.md6
-rw-r--r--doc/api/commits.md87
-rw-r--r--doc/api/labels.md2
-rw-r--r--doc/api/oauth2.md24
-rw-r--r--doc/api/projects.md454
-rw-r--r--doc/api/settings.md12
-rw-r--r--doc/api/users.md9
-rw-r--r--doc/ci/docker/using_docker_images.md2
-rw-r--r--doc/ci/environments.md13
-rw-r--r--doc/ci/examples/README.md3
-rw-r--r--doc/ci/examples/test-phoenix-application.md56
-rw-r--r--doc/ci/pipelines.md2
-rw-r--r--doc/ci/triggers/README.md2
-rw-r--r--doc/ci/variables/README.md36
-rw-r--r--doc/ci/yaml/README.md34
-rw-r--r--doc/container_registry/README.md99
-rw-r--r--doc/container_registry/img/container_registry.pngbin222782 -> 0 bytes
-rw-r--r--doc/container_registry/img/project_feature.pngbin248750 -> 0 bytes
-rw-r--r--doc/container_registry/troubleshooting.md142
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/code_review.md11
-rw-r--r--doc/development/frontend.md225
-rw-r--r--doc/development/licensing.md2
-rw-r--r--doc/development/migration_style_guide.md8
-rw-r--r--doc/gitlab-basics/README.md6
-rw-r--r--doc/gitlab-basics/add-file.md28
-rw-r--r--doc/gitlab-basics/add-merge-request.md49
-rw-r--r--doc/gitlab-basics/basicsimages/add_new_merge_request.pngbin9003 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/add_sshkey.pngbin1394 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_info.pngbin7572 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branch_name.pngbin2137 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/branches.pngbin3548 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/button-create-mr.pngbin5927 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/click-on-new-group.pngbin1957 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_changes.pngbin4941 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commit_message.pngbin5103 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/commits.pngbin4112 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/compare_branches.pngbin1520 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_file.pngbin2451 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/create_group.pngbin3184 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/edit_file.pngbin2221 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_located.pngbin3078 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/file_name.pngbin2412 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_file.pngbin8426 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/find_group.pngbin5897 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/fork.pngbin896 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/group_info.pngbin15479 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/groups.pngbin4752 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/https.pngbin2822 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/image_file.pngbin2796 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issue_title.pngbin8311 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/issues.pngbin4153 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/key.pngbin1177 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/merge_requests.pngbin4213 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_issue.pngbin2974 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_merge_request.pngbin3162 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/new_project.pngbin2234 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/newbranch.pngbin1244 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/paste_sshkey.pngbin7699 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/profile_settings.pngbin1101 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/project_info.pngbin21041 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group.pngbin6034 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select-group2.pngbin5040 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_branch.pngbin11207 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/select_project.pngbin16176 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/settings.pngbin4149 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/shh_keys.pngbin4782 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/submit_new_issue.pngbin8644 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/title_description_mr.pngbin11919 -> 0 bytes
-rw-r--r--doc/gitlab-basics/basicsimages/white_space.pngbin2192 -> 0 bytes
-rw-r--r--doc/gitlab-basics/command-line-commands.md26
-rw-r--r--doc/gitlab-basics/create-branch.md43
-rw-r--r--doc/gitlab-basics/create-group.md51
-rw-r--r--doc/gitlab-basics/create-issue.md33
-rw-r--r--doc/gitlab-basics/create-project.md27
-rw-r--r--doc/gitlab-basics/create-your-ssh-keys.md38
-rw-r--r--doc/gitlab-basics/fork-project.md19
-rw-r--r--doc/gitlab-basics/img/create_new_group_info.pngbin0 -> 53103 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_group_sidebar.pngbin0 -> 5396 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin0 -> 10050 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_from_group.pngbin0 -> 6545 bytes
-rw-r--r--doc/gitlab-basics/img/create_new_project_info.pngbin0 -> 49451 bytes
-rw-r--r--doc/gitlab-basics/img/fork_choose_namespace.pngbin0 -> 39253 bytes
-rw-r--r--doc/gitlab-basics/img/fork_new.pngbin0 -> 25540 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_new.pngbin0 -> 3596 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_page.pngbin0 -> 91432 bytes
-rw-r--r--doc/gitlab-basics/img/merge_request_select_branch.pngbin0 -> 50707 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_button.pngbin0 -> 3070 bytes
-rw-r--r--doc/gitlab-basics/img/new_issue_page.pngbin0 -> 53268 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings.pngbin0 -> 5975 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys.pngbin0 -> 42977 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.pngbin0 -> 37486 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.pngbin0 -> 18498 bytes
-rw-r--r--doc/gitlab-basics/img/profile_settings_ssh_keys_title.pngbin0 -> 2362 bytes
-rw-r--r--doc/gitlab-basics/img/project_clone_url.pngbin0 -> 40490 bytes
-rw-r--r--doc/gitlab-basics/img/project_navbar.pngbin0 -> 5745 bytes
-rw-r--r--doc/gitlab-basics/img/public_file_link.png (renamed from doc/gitlab-basics/basicsimages/public_file_link.png)bin3023 -> 3023 bytes
-rw-r--r--doc/gitlab-basics/img/select_group_dropdown.pngbin0 -> 8038 bytes
-rw-r--r--doc/gitlab-basics/start-using-git.md20
-rw-r--r--doc/install/installation.md10
-rw-r--r--doc/raketasks/backup_restore.md59
-rw-r--r--doc/raketasks/user_management.md15
-rw-r--r--doc/university/README.md215
-rw-r--r--doc/university/glossary/README.md482
-rw-r--r--doc/university/high-availability/aws/README.md387
-rw-r--r--doc/university/high-availability/aws/img/auto-scaling-det.pngbin0 -> 106157 bytes
-rw-r--r--doc/university/high-availability/aws/img/db-subnet-group.pngbin0 -> 98632 bytes
-rw-r--r--doc/university/high-availability/aws/img/ec-subnet.pngbin0 -> 91922 bytes
-rw-r--r--doc/university/high-availability/aws/img/elastic-file-system.pngbin0 -> 109719 bytes
-rw-r--r--doc/university/high-availability/aws/img/ig-rt.pngbin0 -> 42022 bytes
-rw-r--r--doc/university/high-availability/aws/img/ig.pngbin0 -> 26220 bytes
-rw-r--r--doc/university/high-availability/aws/img/instance_specs.pngbin0 -> 40938 bytes
-rw-r--r--doc/university/high-availability/aws/img/new_vpc.pngbin0 -> 54072 bytes
-rw-r--r--doc/university/high-availability/aws/img/policies.pngbin0 -> 132366 bytes
-rw-r--r--doc/university/high-availability/aws/img/rds-net-opt.pngbin0 -> 54996 bytes
-rw-r--r--doc/university/high-availability/aws/img/rds-sec-group.pngbin0 -> 43950 bytes
-rw-r--r--doc/university/high-availability/aws/img/redis-cluster-det.pngbin0 -> 81524 bytes
-rw-r--r--doc/university/high-availability/aws/img/redis-net.pngbin0 -> 100700 bytes
-rw-r--r--doc/university/high-availability/aws/img/route_table.pngbin0 -> 39611 bytes
-rw-r--r--doc/university/high-availability/aws/img/subnet.pngbin0 -> 56466 bytes
-rw-r--r--doc/university/process/README.md30
-rw-r--r--doc/university/support/README.md188
-rw-r--r--doc/university/training/end-user/README.md420
-rw-r--r--doc/update/8.0-to-8.1.md4
-rw-r--r--doc/update/8.1-to-8.2.md4
-rw-r--r--doc/update/8.10-to-8.11.md8
-rw-r--r--doc/update/8.11-to-8.12.md8
-rw-r--r--doc/update/8.12-to-8.13.md205
-rw-r--r--doc/update/8.2-to-8.3.md4
-rw-r--r--doc/update/8.3-to-8.4.md4
-rw-r--r--doc/update/8.4-to-8.5.md4
-rw-r--r--doc/update/8.5-to-8.6.md4
-rw-r--r--doc/update/8.6-to-8.7.md4
-rw-r--r--doc/update/8.7-to-8.8.md4
-rw-r--r--doc/update/8.8-to-8.9.md4
-rw-r--r--doc/update/8.9-to-8.10.md4
-rw-r--r--doc/user/markdown.md10
-rw-r--r--doc/user/permissions.md1
-rw-r--r--doc/user/project/container_registry.md253
-rw-r--r--doc/user/project/cycle_analytics.md88
-rw-r--r--doc/user/project/img/container_registry_enable.pngbin0 -> 5526 bytes
-rw-r--r--doc/user/project/img/container_registry_panel.pngbin0 -> 96315 bytes
-rw-r--r--doc/user/project/img/container_registry_tab.pngbin0 -> 7284 bytes
-rw-r--r--doc/user/project/img/cycle_analytics_landing_page.pngbin58203 -> 66080 bytes
-rw-r--r--doc/user/project/img/mitmproxy-docker.png (renamed from doc/container_registry/img/mitmproxy-docker.png)bin407004 -> 407004 bytes
-rw-r--r--doc/user/project/issue_board.md7
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md3
-rw-r--r--doc/user/project/repository/img/web_editor_new_branch_from_issue.pngbin0 -> 4728 bytes
-rw-r--r--doc/user/project/repository/web_editor.md4
-rw-r--r--doc/user/project/settings/import_export.md3
-rw-r--r--doc/user/project/slash_commands.md3
-rw-r--r--doc/workflow/importing/migrating_from_svn.md108
-rw-r--r--features/profile/ssh_keys.feature20
-rw-r--r--features/project/commits/commits.feature5
-rw-r--r--features/project/issues/issues.feature1
-rw-r--r--features/project/snippets.feature2
-rw-r--r--features/project/source/browse_files.feature17
-rw-r--r--features/steps/dashboard/new_project.rb1
-rw-r--r--features/steps/profile/profile.rb2
-rw-r--r--features/steps/profile/ssh_keys.rb46
-rw-r--r--features/steps/project/commits/commits.rb8
-rw-r--r--features/steps/project/fork.rb1
-rw-r--r--features/steps/project/forked_merge_requests.rb14
-rw-r--r--features/steps/project/issues/issues.rb3
-rw-r--r--features/steps/project/snippets.rb4
-rw-r--r--features/steps/project/source/browse_files.rb4
-rw-r--r--lib/api/access_requests.rb78
-rw-r--r--lib/api/api.rb8
-rw-r--r--lib/api/award_emoji.rb64
-rw-r--r--lib/api/boards.rb115
-rw-r--r--lib/api/commits.rb36
-rw-r--r--lib/api/entities.rb33
-rw-r--r--lib/api/groups.rb3
-rw-r--r--lib/api/helpers.rb7
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/keys.rb7
-rw-r--r--lib/api/members.rb113
-rw-r--r--lib/api/milestones.rb3
-rw-r--r--lib/api/namespaces.rb22
-rw-r--r--lib/api/projects.rb62
-rw-r--r--lib/api/users.rb6
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb24
-rw-r--r--lib/banzai/filter/html_entity_filter.rb12
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb2
-rw-r--r--lib/banzai/filter/syntax_highlight_filter.rb2
-rw-r--r--lib/banzai/filter/task_list_filter.rb20
-rw-r--r--lib/banzai/filter/user_reference_filter.rb14
-rw-r--r--lib/banzai/note_renderer.rb5
-rw-r--r--lib/banzai/object_renderer.rb53
-rw-r--r--lib/banzai/pipeline/single_line_pipeline.rb1
-rw-r--r--lib/banzai/reference_parser/base_parser.rb6
-rw-r--r--lib/banzai/renderer.rb28
-rw-r--r--lib/ci/api/builds.rb6
-rw-r--r--lib/ci/api/helpers.rb29
-rw-r--r--lib/ci/gitlab_ci_yaml_processor.rb2
-rw-r--r--lib/ci/version_info.rb52
-rw-r--r--lib/constraints/group_url_constrainer.rb7
-rw-r--r--lib/constraints/namespace_url_constrainer.rb13
-rw-r--r--lib/constraints/user_url_constrainer.rb7
-rw-r--r--lib/event_filter.rb23
-rw-r--r--lib/gitlab/access.rb4
-rw-r--r--lib/gitlab/auth.rb2
-rw-r--r--lib/gitlab/backend/shell.rb46
-rw-r--r--lib/gitlab/github_import/client.rb21
-rw-r--r--lib/gitlab/github_import/importer.rb141
-rw-r--r--lib/gitlab/github_import/project_creator.rb35
-rw-r--r--lib/gitlab/identifier.rb58
-rw-r--r--lib/gitlab/import_export/attribute_cleaner.rb13
-rw-r--r--lib/gitlab/import_export/command_line_util.rb9
-rw-r--r--lib/gitlab/import_export/file_importer.rb2
-rw-r--r--lib/gitlab/import_export/import_export.yml8
-rw-r--r--lib/gitlab/import_export/project_tree_restorer.rb13
-rw-r--r--lib/gitlab/import_export/project_tree_saver.rb4
-rw-r--r--lib/gitlab/import_export/relation_factory.rb12
-rw-r--r--lib/gitlab/import_export/repo_restorer.rb2
-rw-r--r--lib/gitlab/import_export/repo_saver.rb2
-rw-r--r--lib/gitlab/import_export/version_saver.rb4
-rw-r--r--lib/gitlab/import_export/wiki_repo_saver.rb2
-rw-r--r--lib/gitlab/ldap/access.rb2
-rw-r--r--lib/gitlab/ldap/adapter.rb3
-rw-r--r--lib/gitlab/lfs_token.rb14
-rw-r--r--lib/gitlab/redis.rb34
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/sidekiq_middleware/arguments_logger.rb2
-rw-r--r--lib/gitlab/workhorse.rb14
-rw-r--r--lib/tasks/cache.rake43
-rw-r--r--lib/tasks/flog.rake25
-rw-r--r--lib/tasks/gitlab/shell.rake2
-rw-r--r--lib/tasks/gitlab/users.rake11
-rw-r--r--public/deploy.html7
-rw-r--r--public/robots.txt2
-rwxr-xr-xscripts/prepare_build.sh17
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb7
-rw-r--r--spec/controllers/namespaces_controller_spec.rb118
-rw-r--r--spec/controllers/projects/blob_controller_spec.rb1
-rw-r--r--spec/controllers/projects/boards/issues_controller_spec.rb64
-rw-r--r--spec/controllers/projects/boards/lists_controller_spec.rb3
-rw-r--r--spec/controllers/projects/group_links_controller_spec.rb37
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb14
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb4
-rw-r--r--spec/controllers/projects/repositories_controller_spec.rb2
-rw-r--r--spec/controllers/projects/templates_controller_spec.rb2
-rw-r--r--spec/controllers/projects_controller_spec.rb22
-rw-r--r--spec/controllers/sessions_controller_spec.rb38
-rw-r--r--spec/controllers/users_controller_spec.rb4
-rw-r--r--spec/factories/project_members.rb23
-rw-r--r--spec/factories/projects.rb24
-rw-r--r--spec/features/boards/boards_spec.rb64
-rw-r--r--spec/features/boards/new_issue_spec.rb80
-rw-r--r--spec/features/calendar_spec.rb111
-rw-r--r--spec/features/compare_spec.rb26
-rw-r--r--spec/features/dashboard/snippets_spec.rb15
-rw-r--r--spec/features/dashboard_issues_spec.rb3
-rw-r--r--spec/features/environments_spec.rb28
-rw-r--r--spec/features/expand_collapse_diffs_spec.rb40
-rw-r--r--spec/features/groups_spec.rb32
-rw-r--r--spec/features/issues/filter_by_labels_spec.rb129
-rw-r--r--spec/features/issues/filter_issues_spec.rb43
-rw-r--r--spec/features/issues/form_spec.rb119
-rw-r--r--spec/features/issues/move_spec.rb2
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb82
-rw-r--r--spec/features/issues_spec.rb23
-rw-r--r--spec/features/merge_requests/filter_by_milestone_spec.rb3
-rw-r--r--spec/features/merge_requests/form_spec.rb273
-rw-r--r--spec/features/merge_requests/merge_request_versions_spec.rb19
-rw-r--r--spec/features/merge_requests/user_uses_slash_commands_spec.rb55
-rw-r--r--spec/features/notes_on_merge_requests_spec.rb12
-rw-r--r--spec/features/profiles/keys_spec.rb47
-rw-r--r--spec/features/projects/badges/coverage_spec.rb2
-rw-r--r--spec/features/projects/files/edit_file_soft_wrap_spec.rb41
-rw-r--r--spec/features/projects/import_export/export_file_spec.rb2
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb4
-rw-r--r--spec/features/projects/issuable_templates_spec.rb30
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb4
-rw-r--r--spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb4
-rw-r--r--spec/features/projects/snippets_spec.rb14
-rw-r--r--spec/features/projects_spec.rb6
-rw-r--r--spec/features/runners_spec.rb4
-rw-r--r--spec/features/snippets_spec.rb14
-rw-r--r--spec/features/todos/todos_spec.rb8
-rw-r--r--spec/features/unsubscribe_links_spec.rb2
-rw-r--r--spec/features/users/snippets_spec.rb22
-rw-r--r--spec/features/users_spec.rb11
-rw-r--r--spec/finders/access_requests_finder_spec.rb89
-rw-r--r--spec/finders/joined_groups_finder_spec.rb2
-rw-r--r--spec/finders/projects_finder_spec.rb2
-rw-r--r--spec/finders/trending_projects_finder_spec.rb39
-rw-r--r--spec/helpers/broadcast_messages_helper_spec.rb4
-rw-r--r--spec/helpers/issuables_helper_spec.rb105
-rw-r--r--spec/helpers/issues_helper_spec.rb36
-rw-r--r--spec/helpers/members_helper_spec.rb4
-rw-r--r--spec/helpers/projects_helper_spec.rb4
-rw-r--r--spec/javascripts/activities_spec.js.es661
-rw-r--r--spec/javascripts/fixtures/event_filter.html.haml21
-rw-r--r--spec/javascripts/labels_issue_sidebar_spec.js.es613
-rw-r--r--spec/javascripts/search_autocomplete_spec.js23
-rw-r--r--spec/lib/banzai/filter/html_entity_filter_spec.rb14
-rw-r--r--spec/lib/banzai/filter/syntax_highlight_filter_spec.rb8
-rw-r--r--spec/lib/banzai/filter/task_list_filter_spec.rb16
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb9
-rw-r--r--spec/lib/banzai/note_renderer_spec.rb3
-rw-r--r--spec/lib/banzai/object_renderer_spec.rb62
-rw-r--r--spec/lib/banzai/renderer_spec.rb74
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb10
-rw-r--r--spec/lib/constraints/namespace_url_constrainer_spec.rb25
-rw-r--r--spec/lib/constraints/user_url_constrainer_spec.rb10
-rw-r--r--spec/lib/event_filter_spec.rb49
-rw-r--r--spec/lib/gitlab/auth_spec.rb4
-rw-r--r--spec/lib/gitlab/backend/shell_spec.rb9
-rw-r--r--spec/lib/gitlab/badge/coverage/report_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/client_spec.rb2
-rw-r--r--spec/lib/gitlab/github_import/importer_spec.rb13
-rw-r--r--spec/lib/gitlab/github_import/project_creator_spec.rb24
-rw-r--r--spec/lib/gitlab/identifier_spec.rb123
-rw-r--r--spec/lib/gitlab/import_export/attribute_cleaner_spec.rb34
-rw-r--r--spec/lib/gitlab/import_export/attribute_configuration_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/project.json39
-rw-r--r--spec/lib/gitlab/import_export/project_tree_restorer_spec.rb12
-rw-r--r--spec/lib/gitlab/import_export/project_tree_saver_spec.rb5
-rw-r--r--spec/lib/gitlab/import_export/relation_factory_spec.rb125
-rw-r--r--spec/lib/gitlab/ldap/adapter_spec.rb37
-rw-r--r--spec/lib/gitlab/lfs_token_spec.rb8
-rw-r--r--spec/lib/gitlab/redis_spec.rb34
-rw-r--r--spec/lib/gitlab/template/issue_template_spec.rb6
-rw-r--r--spec/lib/gitlab/template/merge_request_template_spec.rb6
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb40
-rw-r--r--spec/mailers/notify_spec.rb57
-rw-r--r--spec/mailers/shared/notify.rb13
-rw-r--r--spec/models/abuse_report_spec.rb4
-rw-r--r--spec/models/build_spec.rb4
-rw-r--r--spec/models/commit_status_spec.rb55
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb181
-rw-r--r--spec/models/concerns/has_status_spec.rb43
-rw-r--r--spec/models/concerns/mentionable_spec.rb22
-rw-r--r--spec/models/cycle_analytics/summary_spec.rb6
-rw-r--r--spec/models/event_spec.rb8
-rw-r--r--spec/models/forked_project_link_spec.rb1
-rw-r--r--spec/models/issue_spec.rb2
-rw-r--r--spec/models/member_spec.rb245
-rw-r--r--spec/models/members/group_member_spec.rb27
-rw-r--r--spec/models/members/project_member_spec.rb50
-rw-r--r--spec/models/merge_request_diff_spec.rb10
-rw-r--r--spec/models/merge_request_spec.rb102
-rw-r--r--spec/models/milestone_spec.rb4
-rw-r--r--spec/models/namespace_spec.rb1
-rw-r--r--spec/models/project_services/custom_issue_tracker_service_spec.rb16
-rw-r--r--spec/models/project_services/hipchat_service_spec.rb2
-rw-r--r--spec/models/project_services/slack_service/issue_message_spec.rb6
-rw-r--r--spec/models/project_services/slack_service/merge_message_spec.rb6
-rw-r--r--spec/models/project_services/slack_service/note_message_spec.rb10
-rw-r--r--spec/models/project_services/slack_service/push_message_spec.rb12
-rw-r--r--spec/models/project_services/slack_service/wiki_page_message_spec.rb6
-rw-r--r--spec/models/project_spec.rb39
-rw-r--r--spec/models/project_team_spec.rb4
-rw-r--r--spec/models/repository_spec.rb28
-rw-r--r--spec/models/service_spec.rb19
-rw-r--r--spec/models/snippet_spec.rb7
-rw-r--r--spec/models/trending_project_spec.rb56
-rw-r--r--spec/models/user_spec.rb17
-rw-r--r--spec/policies/project_policy_spec.rb147
-rw-r--r--spec/requests/api/access_requests_spec.rb32
-rw-r--r--spec/requests/api/api_helpers_spec.rb39
-rw-r--r--spec/requests/api/boards_spec.rb192
-rw-r--r--spec/requests/api/builds_spec.rb27
-rw-r--r--spec/requests/api/commits_spec.rb273
-rw-r--r--spec/requests/api/fork_spec.rb2
-rw-r--r--spec/requests/api/groups_spec.rb10
-rw-r--r--spec/requests/api/internal_spec.rb4
-rw-r--r--spec/requests/api/members_spec.rb16
-rw-r--r--spec/requests/api/merge_requests_spec.rb4
-rw-r--r--spec/requests/api/milestones_spec.rb8
-rw-r--r--spec/requests/api/project_hooks_spec.rb5
-rw-r--r--spec/requests/api/projects_spec.rb43
-rw-r--r--spec/requests/api/settings_spec.rb36
-rw-r--r--spec/requests/api/users_spec.rb59
-rw-r--r--spec/requests/ci/api/builds_spec.rb43
-rw-r--r--spec/requests/git_http_spec.rb656
-rw-r--r--spec/requests/jwt_controller_spec.rb4
-rw-r--r--spec/requests/lfs_http_spec.rb29
-rw-r--r--spec/routing/routing_spec.rb8
-rw-r--r--spec/services/boards/issues/create_service_spec.rb33
-rw-r--r--spec/services/boards/issues/list_service_spec.rb12
-rw-r--r--spec/services/boards/issues/move_service_spec.rb4
-rw-r--r--spec/services/boards/lists/create_service_spec.rb8
-rw-r--r--spec/services/boards/lists/destroy_service_spec.rb8
-rw-r--r--spec/services/boards/lists/generate_service_spec.rb9
-rw-r--r--spec/services/boards/lists/move_service_spec.rb4
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb101
-rw-r--r--spec/services/files/update_service_spec.rb4
-rw-r--r--spec/services/git_push_service_spec.rb2
-rw-r--r--spec/services/issues/create_service_spec.rb36
-rw-r--r--spec/services/issues/update_service_spec.rb97
-rw-r--r--spec/services/members/approve_access_request_service_spec.rb96
-rw-r--r--spec/services/members/destroy_service_spec.rb115
-rw-r--r--spec/services/members/request_access_service_spec.rb57
-rw-r--r--spec/services/merge_requests/build_service_spec.rb20
-rw-r--r--spec/services/merge_requests/merge_service_spec.rb63
-rw-r--r--spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb24
-rw-r--r--spec/services/merge_requests/refresh_service_spec.rb12
-rw-r--r--spec/services/notification_service_spec.rb14
-rw-r--r--spec/services/projects/destroy_service_spec.rb23
-rw-r--r--spec/services/projects/fork_service_spec.rb25
-rw-r--r--spec/services/projects/import_service_spec.rb10
-rw-r--r--spec/services/slash_commands/interpret_service_spec.rb98
-rw-r--r--spec/services/system_note_service_spec.rb22
-rw-r--r--spec/spec_helper.rb7
-rw-r--r--spec/support/cycle_analytics_helpers.rb26
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb (renamed from spec/support/issuable_slash_commands_shared_examples.rb)0
-rw-r--r--spec/support/git_http_helpers.rb48
-rw-r--r--spec/support/import_export/export_file_helper.rb4
-rw-r--r--spec/support/matchers/have_issuable_counts.rb21
-rw-r--r--spec/support/mentionable_shared_examples.rb4
-rw-r--r--spec/support/services/issuable_create_service_slash_commands_shared_examples.rb (renamed from spec/support/issuable_create_service_slash_commands_shared_examples.rb)0
-rw-r--r--spec/support/snippets_shared_examples.rb18
-rw-r--r--spec/tasks/gitlab/users_rake_spec.rb38
-rw-r--r--spec/views/admin/dashboard/index.html.haml_spec.rb2
-rw-r--r--spec/views/ci/lints/show.html.haml_spec.rb97
-rw-r--r--spec/views/projects/builds/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/issues/_related_branches.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/_heading.html.haml_spec.rb2
-rw-r--r--spec/views/projects/merge_requests/edit.html.haml_spec.rb7
-rw-r--r--spec/views/projects/merge_requests/show.html.haml_spec.rb15
-rw-r--r--spec/views/projects/notes/_form.html.haml_spec.rb2
-rw-r--r--spec/views/projects/pipelines/show.html.haml_spec.rb2
-rw-r--r--spec/views/projects/tree/show.html.haml_spec.rb2
-rw-r--r--spec/workers/expire_build_artifacts_worker_spec.rb51
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb69
-rw-r--r--spec/workers/post_receive_spec.rb4
-rw-r--r--spec/workers/process_pipeline_worker_spec.rb22
-rw-r--r--spec/workers/trending_projects_worker_spec.rb11
-rw-r--r--spec/workers/update_pipeline_worker_spec.rb22
-rw-r--r--vendor/gitignore/Erlang.gitignore2
-rw-r--r--vendor/gitignore/Global/Ansible.gitignore1
-rw-r--r--vendor/gitignore/Global/Linux.gitignore3
-rw-r--r--vendor/gitignore/Go.gitignore3
-rw-r--r--vendor/gitignore/Node.gitignore3
-rw-r--r--vendor/gitignore/TeX.gitignore3
-rw-r--r--vendor/gitignore/VisualStudio.gitignore8
-rw-r--r--vendor/gitlab-ci-yml/.gitlab-ci.yml4
-rw-r--r--vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml34
-rw-r--r--vendor/gitlab-ci-yml/Julia.gitlab-ci.yml54
881 files changed, 16015 insertions, 6320 deletions
diff --git a/.csscomb.json b/.csscomb.json
index 741cc1488b5..aa6a17f7517 100644
--- a/.csscomb.json
+++ b/.csscomb.json
@@ -6,7 +6,7 @@
"always-semicolon": true,
"color-case": "lower",
"block-indent": " ",
- "color-shorthand": true,
+ "color-shorthand": false,
"element-case": "lower",
"space-before-colon": "",
"space-after-colon": " ",
diff --git a/.flayignore b/.flayignore
index f120de527bd..44df2ba2371 100644
--- a/.flayignore
+++ b/.flayignore
@@ -1,2 +1,3 @@
*.erb
lib/gitlab/sanitizers/svg/whitelist.rb
+lib/gitlab/diff/position_tracer.rb
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 3f3873e57c1..cb6f691058e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -1,9 +1,8 @@
-image: "ruby:2.3.1"
+image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.3-git-2.7-phantomjs-2.1"
cache:
key: "ruby-231"
paths:
- - vendor/apt
- vendor/ruby
variables:
@@ -20,6 +19,8 @@ variables:
before_script:
- source ./scripts/prepare_build.sh
- cp config/gitlab.yml.example config/gitlab.yml
+ - mkdir -p tmp/tests
+ - mount -t tmpfs tmpfs tmp/tests || echo "tmpfs mount failed, falling back to disc"
- bundle --version
- '[ "$USE_BUNDLE_INSTALL" != "true" ] || retry bundle install --without postgres production --jobs $(nproc) "${FLAGS[@]}"'
- retry gem install knapsack
@@ -141,14 +142,13 @@ spinach 9 10: *spinach-knapsack
# Execute all testing suites against Ruby 2.1
.ruby-21: &ruby-21
- image: "ruby:2.1"
+ image: "dev.gitlab.org:5005/gitlab/gitlab-build-images:ruby-2.1-git-2.7-phantomjs-2.1"
<<: *use-db
only:
- master
cache:
key: "ruby21"
paths:
- - vendor/apt
- vendor/ruby
.rspec-knapsack-ruby21: &rspec-knapsack-ruby21
@@ -209,12 +209,7 @@ rubocop: *exec
rake haml_lint: *exec
rake scss_lint: *exec
rake brakeman: *exec
-rake flog:
- <<: *exec
- allow_failure: yes
-rake flay:
- <<: *exec
- allow_failure: yes
+rake flay: *exec
license_finder: *exec
rake downtime_check: *exec
diff --git a/.rubocop.yml b/.rubocop.yml
index 5bd31ccf329..bec2464c740 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -453,6 +453,10 @@ Style/VariableName:
EnforcedStyle: snake_case
Enabled: true
+# Use the configured style when numbering variables.
+Style/VariableNumber:
+ Enabled: false
+
# Use when x then ... for one-line cases.
Style/WhenThen:
Enabled: true
@@ -639,6 +643,10 @@ Lint/RescueException:
Lint/ShadowedException:
Enabled: false
+# Checks for Object#to_s usage in string interpolation.
+Lint/StringConversionInInterpolation:
+ Enabled: true
+
# Do not use prefix `_` for a variable that is used.
Lint/UnderscorePrefixedVariableName:
Enabled: true
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index 87520c67dd5..11b34fafa2a 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -1,21 +1,21 @@
# This configuration was generated by
# `rubocop --auto-gen-config --exclude-limit 0`
-# on 2016-09-14 15:44:53 -0400 using RuboCop version 0.42.0.
+# on 2016-10-04 13:16:20 +0200 using RuboCop version 0.43.0.
# The point is for the user to remove these configuration records
# one by one as the offenses are removed from the code base.
# Note that changes in the inspected code, or installation of new
# versions of RuboCop, may require this file to be generated again.
-# Offense count: 158
+# Offense count: 160
Lint/AmbiguousRegexpLiteral:
Enabled: false
-# Offense count: 41
+# Offense count: 40
# Configuration parameters: AllowSafeAssignment.
Lint/AssignmentInCondition:
Enabled: false
-# Offense count: 16
+# Offense count: 18
Lint/HandleExceptions:
Enabled: false
@@ -23,16 +23,21 @@ Lint/HandleExceptions:
Lint/Loop:
Enabled: false
-# Offense count: 16
+# Offense count: 19
Lint/ShadowingOuterLocalVariable:
Enabled: false
-# Offense count: 6
+# Offense count: 9
+# Cop supports --auto-correct.
+Lint/UnifiedInteger:
+ Enabled: false
+
+# Offense count: 13
# Cop supports --auto-correct.
-Lint/StringConversionInInterpolation:
+Lint/UnneededSplatExpansion:
Enabled: false
-# Offense count: 49
+# Offense count: 69
# Cop supports --auto-correct.
# Configuration parameters: IgnoreEmptyBlocks, AllowUnusedKeywordArguments.
Lint/UnusedBlockArgument:
@@ -44,32 +49,81 @@ Lint/UnusedBlockArgument:
Lint/UnusedMethodArgument:
Enabled: false
-# Offense count: 9
-# Cop supports --auto-correct.
-Performance/PushSplat:
- Enabled: false
-
# Offense count: 2
# Cop supports --auto-correct.
Performance/RedundantBlockCall:
Enabled: false
-# Offense count: 4
+# Offense count: 5
# Cop supports --auto-correct.
Performance/RedundantMatch:
Enabled: false
-# Offense count: 27
+# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: MaxKeyValuePairs.
Performance/RedundantMerge:
Enabled: false
-# Offense count: 61
+# Offense count: 7
+RSpec/BeEql:
+ Enabled: false
+
+# Offense count: 20
+# Configuration parameters: CustomIncludeMethods.
+RSpec/EmptyExampleGroup:
+ Enabled: false
+
+# Offense count: 16
+RSpec/ExpectActual:
+ Enabled: false
+
+# Offense count: 34
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: implicit, each, example
+RSpec/HookArgument:
+ Enabled: false
+
+# Offense count: 168
+RSpec/LeadingSubject:
+ Enabled: false
+
+# Offense count: 162
+RSpec/LetSetup:
+ Enabled: false
+
+# Offense count: 10
+RSpec/MessageChain:
+ Enabled: false
+
+# Offense count: 714
+# Configuration parameters: EnforcedStyle, SupportedStyles.
+# SupportedStyles: allow, expect
+RSpec/MessageExpectation:
+ Enabled: false
+
+# Offense count: 2423
+RSpec/MultipleExpectations:
+ Max: 36
+
+# Offense count: 1504
+RSpec/NamedSubject:
+ Enabled: false
+
+# Offense count: 1335
+# Configuration parameters: MaxNesting.
+RSpec/NestedGroups:
+ Enabled: false
+
+# Offense count: 99
+RSpec/SubjectStub:
+ Enabled: false
+
+# Offense count: 64
Rails/OutputSafety:
Enabled: false
-# Offense count: 129
+# Offense count: 151
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: strict, flexible
Rails/TimeZone:
@@ -82,58 +136,63 @@ Rails/TimeZone:
Rails/Validation:
Enabled: false
-# Offense count: 273
+# Offense count: 2
+# Cop supports --auto-correct.
+Security/JSONLoad:
+ Enabled: false
+
+# Offense count: 284
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: with_first_parameter, with_fixed_indentation
Style/AlignParameters:
Enabled: false
-# Offense count: 30
+# Offense count: 28
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: always, conditionals
Style/AndOr:
Enabled: false
-# Offense count: 50
+# Offense count: 52
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: percent_q, bare_percent
Style/BarePercentLiterals:
Enabled: false
-# Offense count: 289
+# Offense count: 291
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: braces, no_braces, context_dependent
Style/BracesAroundHashParameters:
Enabled: false
-# Offense count: 5
+# Offense count: 6
Style/CaseEquality:
Enabled: false
-# Offense count: 19
+# Offense count: 26
# Cop supports --auto-correct.
Style/ColonMethodCall:
Enabled: false
-# Offense count: 3
+# Offense count: 2
# Cop supports --auto-correct.
# Configuration parameters: Keywords.
# Keywords: TODO, FIXME, OPTIMIZE, HACK, REVIEW
Style/CommentAnnotation:
Enabled: false
-# Offense count: 33
+# Offense count: 30
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, SingleLineConditionsOnly.
# SupportedStyles: assign_to_condition, assign_inside_condition
Style/ConditionalAssignment:
Enabled: false
-# Offense count: 881
+# Offense count: 957
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: leading, trailing
@@ -144,12 +203,12 @@ Style/DotPosition:
Style/DoubleNegation:
Enabled: false
-# Offense count: 4
+# Offense count: 6
# Cop supports --auto-correct.
Style/EachWithObject:
Enabled: false
-# Offense count: 25
+# Offense count: 26
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: empty, nil, both
@@ -161,24 +220,24 @@ Style/EmptyElse:
Style/EmptyLiteral:
Enabled: false
-# Offense count: 135
+# Offense count: 140
# Cop supports --auto-correct.
# Configuration parameters: AllowForAlignment, ForceEqualSignAlignment.
Style/ExtraSpacing:
Enabled: false
-# Offense count: 7
+# Offense count: 6
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: format, sprintf, percent
Style/FormatString:
Enabled: false
-# Offense count: 51
+# Offense count: 201
# Configuration parameters: MinBodyLength.
Style/GuardClause:
Enabled: false
-# Offense count: 9
+# Offense count: 11
Style/IfInsideElse:
Enabled: false
@@ -188,21 +247,21 @@ Style/IfInsideElse:
Style/IfUnlessModifier:
Enabled: false
-# Offense count: 52
+# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_brackets
Style/IndentArray:
Enabled: false
-# Offense count: 97
+# Offense count: 95
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, IndentationWidth.
# SupportedStyles: special_inside_parentheses, consistent, align_braces
Style/IndentHash:
Enabled: false
-# Offense count: 12
+# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: line_count_dependent, lambda, literal
@@ -214,7 +273,7 @@ Style/Lambda:
Style/LineEndConcatenation:
Enabled: false
-# Offense count: 13
+# Offense count: 15
# Cop supports --auto-correct.
Style/MethodCallParentheses:
Enabled: false
@@ -223,7 +282,7 @@ Style/MethodCallParentheses:
Style/MethodMissing:
Enabled: false
-# Offense count: 85
+# Offense count: 95
# Cop supports --auto-correct.
Style/MutableConstant:
Enabled: false
@@ -240,14 +299,14 @@ Style/NestedParenthesizedCalls:
Style/Next:
Enabled: false
-# Offense count: 8
+# Offense count: 12
# Cop supports --auto-correct.
# Configuration parameters: EnforcedOctalStyle, SupportedOctalStyles.
# SupportedOctalStyles: zero_with_o, zero_only
Style/NumericLiteralPrefix:
Enabled: false
-# Offense count: 64
+# Offense count: 53
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: predicate, comparison
@@ -259,7 +318,7 @@ Style/NumericPredicate:
Style/ParallelAssignment:
Enabled: false
-# Offense count: 264
+# Offense count: 294
# Cop supports --auto-correct.
# Configuration parameters: PreferredDelimiters.
Style/PercentLiteralDelimiters:
@@ -277,7 +336,7 @@ Style/PercentQLiterals:
Style/PerlBackrefs:
Enabled: false
-# Offense count: 35
+# Offense count: 38
# Configuration parameters: NamePrefix, NamePrefixBlacklist, NameWhitelist.
# NamePrefix: is_, has_, have_
# NamePrefixBlacklist: is_, has_, have_
@@ -285,7 +344,7 @@ Style/PerlBackrefs:
Style/PredicateName:
Enabled: false
-# Offense count: 27
+# Offense count: 26
# Cop supports --auto-correct.
Style/PreferredHashMethods:
Enabled: false
@@ -317,12 +376,12 @@ Style/RedundantException:
Style/RedundantFreeze:
Enabled: false
-# Offense count: 408
+# Offense count: 427
# Cop supports --auto-correct.
Style/RedundantSelf:
Enabled: false
-# Offense count: 93
+# Offense count: 97
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, AllowInnerSlashes.
# SupportedStyles: slashes, percent_r, mixed
@@ -334,7 +393,12 @@ Style/RegexpLiteral:
Style/RescueModifier:
Enabled: false
-# Offense count: 5
+# Offense count: 114
+# Cop supports --auto-correct.
+Style/SafeNavigation:
+ Enabled: false
+
+# Offense count: 7
# Cop supports --auto-correct.
Style/SelfAssignment:
Enabled: false
@@ -351,7 +415,7 @@ Style/SingleLineBlockParams:
Style/SingleLineMethods:
Enabled: false
-# Offense count: 124
+# Offense count: 125
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: space, no_space
@@ -364,19 +428,19 @@ Style/SpaceBeforeBlockBraces:
Style/SpaceBeforeFirstArg:
Enabled: false
-# Offense count: 141
+# Offense count: 145
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles, EnforcedStyleForEmptyBraces, SpaceBeforeBlockParameters.
# SupportedStyles: space, no_space
Style/SpaceInsideBlockBraces:
Enabled: false
-# Offense count: 96
+# Offense count: 99
# Cop supports --auto-correct.
Style/SpaceInsideBrackets:
Enabled: false
-# Offense count: 62
+# Offense count: 65
# Cop supports --auto-correct.
Style/SpaceInsideParens:
Enabled: false
@@ -386,21 +450,21 @@ Style/SpaceInsideParens:
Style/SpaceInsidePercentLiteralDelimiters:
Enabled: false
-# Offense count: 40
+# Offense count: 41
# Cop supports --auto-correct.
# Configuration parameters: SupportedStyles.
# SupportedStyles: use_perl_names, use_english_names
Style/SpecialGlobalVars:
EnforcedStyle: use_perl_names
-# Offense count: 30
+# Offense count: 31
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle, SupportedStyles.
# SupportedStyles: single_quotes, double_quotes
Style/StringLiteralsInInterpolation:
Enabled: false
-# Offense count: 32
+# Offense count: 33
# Cop supports --auto-correct.
# Configuration parameters: IgnoredMethods.
# IgnoredMethods: respond_to, define_method
@@ -414,7 +478,7 @@ Style/SymbolProc:
Style/TernaryParentheses:
Enabled: false
-# Offense count: 24
+# Offense count: 29
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyleForMultiline, SupportedStyles.
# SupportedStyles: comma, consistent_comma, no_comma
diff --git a/.scss-lint.yml b/.scss-lint.yml
index 66f9975d4ce..71df6be6a15 100644
--- a/.scss-lint.yml
+++ b/.scss-lint.yml
@@ -79,7 +79,7 @@ linters:
# HEX colors should use three-character values where possible.
HexLength:
- enabled: true
+ enabled: false
# HEX color values should use lower-case colors to differentiate between
# letters and numbers, e.g. `#E3E3E3` vs. `#e3e3e3`.
diff --git a/CHANGELOG b/CHANGELOG
index 592a3fec134..2095e4fc0fd 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,9 +1,136 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.13.0 (unreleased)
+ - Truncate long labels with ellipsis in labels page
+ - Bump mail_room to v0.8.1 to fix thread cleanup issue
+ - Update runner version only when updating contacted_at
+ - Add link from system note to compare with previous version
+ - Improve issue load time performance by avoiding ORDER BY in find_by call
+ - Use gitlab-shell v3.6.2 (GIT TRACE logging)
+ - Add `/projects/visible` API endpoint (Ben Boeckel)
+ - Fix centering of custom header logos (Ashley Dumaine)
+ - ExpireBuildArtifactsWorker query builds table without ordering enqueuing one job per build to cleanup
+ - Add an example for testing a phoenix application with Gitlab CI in the docs (Manthan Mallikarjun)
+ - AbstractReferenceFilter caches project_refs on RequestStore when active
+ - Replaced the check sign to arrow in the show build view. !6501
+ - Add a /wip slash command to toggle the Work In Progress status of a merge request. !6259 (tbalthazar)
+ - Fix Error 500 when viewing old merge requests with bad diff data
- Speed-up group milestones show page
-
-v 8.12.2 (unreleased)
+ - Fix inconsistent options dropdown caret on mobile viewports (ClemMakesApps)
+ - Don't include archived projects when creating group milestones. !4940 (Jeroen Jacobs)
+ - Add tag shortcut from the Commit page. !6543
+ - Keep refs for each deployment
+ - Log LDAP lookup errors and don't swallow unrelated exceptions. !6103 (Markus Koller)
+ - Add more tests for calendar contribution (ClemMakesApps)
+ - Update Gitlab Shell to fix some problems with moving projects between storages
+ - Cache rendered markdown in the database, rather than Redis
+ - Avoid database queries on Banzai::ReferenceParser::BaseParser for nodes without references
+ - Simplify Mentionable concern instance methods
+ - Fix permission for setting an issue's due date
+ - API: Multi-file commit !6096 (mahcsig)
+ - Revert "Label list shows all issues (opened or closed) with that label"
+ - Expose expires_at field when sharing project on API
+ - Fix VueJS template tags being rendered in code comments
+ - Added copy file path button to merge request diff files
+ - Fix issue with page scrolling to top when closing or pinning sidebar (lukehowell)
+ - Add Issue Board API support (andrebsguedes)
+ - Allow the Koding integration to be configured through the API
+ - Add new issue button to each list on Issues Board
+ - Added soft wrap button to repository file/blob editor
+ - Update namespace validation to forbid reserved names (.git and .atom) (Will Starms)
+ - Add word-wrap to issue title on issue and milestone boards (ClemMakesApps)
+ - Fix todos page mobile viewport layout (ClemMakesApps)
+ - Fix inconsistent highlighting of already selected activity nav-links (ClemMakesApps)
+ - Remove redundant mixins (ClemMakesApps)
+ - Fix robots.txt disallowing access to groups starting with "s" (Matt Harrison)
+ - Close open merge request without source project (Katarzyna Kobierska Ula Budziszewska)
+ - Fix that manual jobs would no longer block jobs in the next stage. !6604
+ - Add configurable email subject suffix (Fu Xu)
+ - Added tooltip to fork count on project show page. (Justin DiPierro)
+ - Use a ConnectionPool for Rails.cache on Sidekiq servers
+ - Replace `alias_method_chain` with `Module#prepend`
+ - Enable GitLab Import/Export for non-admin users.
+ - Preserve label filters when sorting !6136 (Joseph Frazier)
+ - MergeRequest#new form load diff asynchronously
+ - Only update issuable labels if they have been changed
+ - Take filters in account in issuable counters. !6496
+ - Use custom Ruby images to test builds (registry.dev.gitlab.org/gitlab/gitlab-build-images:*)
+ - Prevent flash alert text from being obscured when container is fluid
+ - Append issue template to existing description !6149 (Joseph Frazier)
+ - Trending projects now only show public projects and the list of projects is cached for a day
+ - Memoize Gitlab Shell's secret token (!6599, Justin DiPierro)
+ - Revoke button in Applications Settings underlines on hover.
+ - Use higher size on Gitlab::Redis connection pool on Sidekiq servers
+ - Add missing values to linter !6276 (Katarzyna Kobierska Ula Budziszewska)
+ - Fix Long commit messages overflow viewport in file tree
+ - Revert avoid touching file system on Build#artifacts?
+ - Stop using a Redis lease when updating the project activity timestamp whenever a new event is created
+ - Add broadcast messages and alerts below sub-nav
+ - Better empty state for Groups view
+ - Update ruby-prof to 0.16.2. !6026 (Elan Ruusamäe)
+ - Replace bootstrap caret with fontawesome caret (ClemMakesApps)
+ - Fix unnecessary escaping of reserved HTML characters in milestone title. !6533
+ - Add organization field to user profile
+ - Fix enter key when navigating search site search dropdown. !6643 (Brennan Roberts)
+ - Fix deploy status responsiveness error !6633
+ - Make searching for commits case insensitive
+ - Fix resolved discussion display in side-by-side diff view !6575
+ - Optimize GitHub importing for speed and memory
+ - API: expose pipeline data in builds API (!6502, Guilherme Salazar)
+ - Notify the Merger about merge after successful build (Dimitris Karakasilis)
+ - Reorder issue and merge request titles to show IDs first. !6503 (Greg Laubenstein)
+ - Add a new gitlab:users:clear_all_authentication_tokens task. !6745
+ - Reduce queries needed to find users using their SSH keys when pushing commits
+ - Prevent rendering the link to all when the author has no access (Katarzyna Kobierska Ula Budziszewska)
+ - Fix broken repository 500 errors in project list
+ - Fix Pipeline list commit column width should be adjusted
+ - Close todos when accepting merge requests via the API !6486 (tonygambone)
+ - Changed Slack service user referencing from full name to username (Sebastian Poxhofer)
+ - Retouch environments list and deployments list
+ - Add Container Registry on/off status to Admin Area !6638 (the-undefined)
+ - Allow empty merge requests !6384 (Artem Sidorenko)
+ - Grouped pipeline dropdown is a scrollable container
+ - Cleanup Ci::ApplicationController. !6757 (Takuya Noguchi)
+ - Fix a typo in doc/api/labels.md
+ - API: all unknown routing will be handled with 404 Not Found
+
+v 8.12.5 (unreleased)
+ - Update the mail_room gem to 0.8.1 to fix a race condition with the mailbox watching thread
+
+v 8.12.4
+ - Fix "Copy to clipboard" tooltip to say "Copied!" when clipboard button is clicked. !6294 (lukehowell)
+ - Fix padding in build sidebar. !6506
+ - Changed compare dropdowns to dropdowns with isolated search input. !6550
+ - Fix race condition on LFS Token. !6592
+ - Fix type mismatch bug when closing Jira issue. !6619
+ - Fix lint-doc error. !6623
+ - Skip wiki creation when GitHub project has wiki enabled. !6665
+ - Fix issues importing services via Import/Export. !6667
+ - Restrict failed login attempts for users with 2FA enabled. !6668
+ - Fix failed project deletion when feature visibility set to private. !6688
+ - Prevent claiming associated model IDs via import.
+ - Set GitLab project exported file permissions to owner only
+ - Change user & group landing page routing from /u/:username to /:username
+
+v 8.12.3
+ - Update Gitlab Shell to support low IO priority for storage moves
+
+v 8.12.2
+ - Fix Import/Export not recognising correctly the imported services.
+ - Fix snippets pagination
+ - Fix "Create project" button layout when visibility options are restricted
+ - Fix List-Unsubscribe header in emails
+ - Fix IssuesController#show degradation including project on loaded notes
+ - Fix an issue with the "Commits" section of the cycle analytics summary. !6513
+ - Fix errors importing project feature and milestone models using GitLab project import
+ - Make JWT messages Docker-compatible
+ - Fix duplicate branch entry in the merge request version compare dropdown
+ - Respect the fork_project permission when forking projects
+ - Only update issuable labels if they have been changed
+ - Fix bug where 'Search results' repeated many times when a search in the emoji search form is cleared (Xavier Bick) (@zeiv)
+ - Fix resolve discussion buttons endpoint path
+ - Refactor remnants of CoffeeScript destructured opts and super !6261
+ - Prevent running GfmAutocomplete setup for each diff note !6569
v 8.12.1
- Fix a memory leak in HTML::Pipeline::SanitizationFilter::WHITELIST
@@ -17,6 +144,7 @@ v 8.12.0
- Allow to set request_access_enabled for groups and projects
- Cleanup misalignments in Issue list view !6206
- Only create a protected branch upon a push to a new branch if a rule for that branch doesn't exist
+ - Add Pipelines for Commit
- Prune events older than 12 months. (ritave)
- Prepend blank line to `Closes` message on merge request linked to issue (lukehowell)
- Fix issues/merge-request templates dropdown for forked projects
@@ -43,6 +171,7 @@ v 8.12.0
- Fix long comments in diffs messing with table width
- Add spec covering 'Gitlab::Git::committer_hash' !6433 (dandunckelman)
- Fix pagination on user snippets page
+ - Honor "fixed layout" preference in more places !6422
- Run CI builds with the permissions of users !5735
- Fix sorting of issues in API
- Fix download artifacts button links !6407
@@ -59,6 +188,7 @@ v 8.12.0
- Reduce contributions calendar data payload (ClemMakesApps)
- Show all pipelines for merge requests even from discarded commits !6414
- Replace contributions calendar timezone payload with dates (ClemMakesApps)
+ - Changed MR widget build status to pipeline status !6335
- Add `web_url` field to issue, merge request, and snippet API objects (Ben Boeckel)
- Enable pipeline events by default !6278
- Move parsing of sidekiq ps into helper !6245 (pascalbetz)
@@ -82,6 +212,7 @@ v 8.12.0
- Increase ci_builds artifacts_size column to 8-byte integer to allow larger files
- Add textarea autoresize after comment (ClemMakesApps)
- Do not write SSH public key 'comments' to authorized_keys !6381
+ - Add due date to issue todos
- Refresh todos count cache when an Issue/MR is deleted
- Fix branches page dropdown sort alignment (ClemMakesApps)
- Hides merge request button on branches page is user doesn't have permissions
@@ -108,6 +239,7 @@ v 8.12.0
- Change pipeline duration to be jobs running time instead of simple wall time from start to end !6084
- Show queued time when showing a pipeline !6084
- Remove unused mixins (ClemMakesApps)
+ - Fix issue board label filtering appending already filtered labels
- Add search to all issue board lists
- Scroll active tab into view on mobile
- Fix groups sort dropdown alignment (ClemMakesApps)
@@ -192,10 +324,17 @@ v 8.12.0
- Fix non-master branch readme display in tree view
- Add UX improvements for merge request version diffs
+v 8.11.8
+ - Respect the fork_project permission when forking projects
+ - Set a restrictive CORS policy on the API for credentialed requests
+ - API: disable rails session auth for non-GET/HEAD requests
+ - Escape HTML nodes in builds commands in CI linter
+
v 8.11.7
- Avoid conflict with admin labels when importing GitHub labels. !6158
- Restores `fieldName` to allow only string values in `gl_dropdown.js`. !6234
- Allow the Rails cookie to be used for API authentication.
+ - Updating verbiage on git basics to be more intuitive
v 8.11.6
- Fix unnecessary horizontal scroll area in pipeline visualizations. !6005
@@ -356,6 +495,7 @@ v 8.11.0
- Add pipeline events hook
- Bump gitlab_git to speedup DiffCollection iterations
- Rewrite description of a blocked user in admin settings. (Elias Werberich)
+ - Clarify documentation for Runners API (Gennady Trafimenkov)
- Make branches sortable without push permission !5462 (winniehell)
- Check for Ci::Build artifacts at database level on pipeline partial
- Convert image diff background image to CSS (ClemMakesApps)
@@ -411,6 +551,12 @@ v 8.11.0
- Update gitlab_git gem to 10.4.7
- Simplify SQL queries of marking a todo as done
+v 8.10.11
+ - Respect the fork_project permission when forking projects
+ - Set a restrictive CORS policy on the API for credentialed requests
+ - API: disable rails session auth for non-GET/HEAD requests
+ - Escape HTML nodes in builds commands in CI linter
+
v 8.10.10
- Allow the Rails cookie to be used for API authentication.
@@ -647,6 +793,12 @@ v 8.10.0
- Show tooltip on GitLab export link in new project page
- Fix import_data wrongly saved as a result of an invalid import_url !5206
+v 8.9.11
+ - Respect the fork_project permission when forking projects
+ - Set a restrictive CORS policy on the API for credentialed requests
+ - API: disable rails session auth for non-GET/HEAD requests
+ - Escape HTML nodes in builds commands in CI linter
+
v 8.9.10
- Allow the Rails cookie to be used for API authentication.
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index d5e15bfce14..0cdcb54b0ae 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -226,8 +226,7 @@ a feedback issue (if there isn't one already) and leave a comment asking for it
to be marked as `Accepting merge requests`. Please include screenshots or
wireframes if the feature will also change the UI.
-Merge requests can be filed either at [GitLab.com][gitlab-mr-tracker] or at
-[github.com][github-mr-tracker].
+Merge requests should be opened at [GitLab.com][gitlab-mr-tracker].
If you are new to GitLab development (or web development in general), see the
[I want to contribute!](#i-want-to-contribute) section to get you started with
@@ -246,10 +245,17 @@ tests are least likely to receive timely feedback. The workflow to make a merge
request is as follows:
1. Fork the project into your personal space on GitLab.com
-1. Create a feature branch, branch away from `master`.
+1. Create a feature branch, branch away from `master`
1. Write [tests](https://gitlab.com/gitlab-org/gitlab-development-kit#running-the-tests) and code
-1. Add your changes to the [CHANGELOG](CHANGELOG)
-1. If you are writing documentation, make sure to read the [documentation styleguide][doc-styleguide]
+1. Add your changes to the [CHANGELOG](CHANGELOG):
+ 1. If you are fixing a ~regression issue, you can add your entry to the next
+ patch release (e.g. `8.12.5` if current version is `8.12.4`)
+ 1. Otherwise, add your entry to the next minor release (e.g. `8.13.0` if
+ current version is `8.12.4`
+ 1. Please add your entry at a random place among the entries of the targeted
+ release
+1. If you are writing documentation, make sure to follow the
+ [documentation styleguide][doc-styleguide]
1. If you have multiple commits please combine them into one commit by
[squashing them][git-squash]
1. Push the commit(s) to your fork
@@ -258,7 +264,7 @@ request is as follows:
1. The MR description should give a motive for your change and the method you
used to achieve it, see the [merge request description format]
(#merge-request-description-format)
-1. If the MR changes the UI it should include before and after screenshots
+1. If the MR changes the UI it should include *Before* and *After* screenshots
1. If the MR changes CSS classes please include the list of affected pages,
`grep css-class ./app -R`
1. Link any relevant [issues][ce-tracker] in the merge request description and
@@ -270,7 +276,9 @@ request is as follows:
[shell command guidelines](doc/development/shell_commands.md)
1. If your code creates new files on disk please read the
[shared files guidelines](doc/development/shared_files.md).
-1. When writing commit messages please follow [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) [guidelines](http://chris.beams.io/posts/git-commit/).
+1. When writing commit messages please follow
+ [these](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html)
+ [guidelines](http://chris.beams.io/posts/git-commit/).
1. If your merge request adds one or more migrations, make sure to execute all
migrations on a fresh database before the MR is reviewed. If the review leads
to large changes in the MR, do this again once the review is complete.
@@ -305,23 +313,6 @@ Please ensure that your merge request meets the contribution acceptance criteria
When having your code reviewed and when reviewing merge requests please take the
[code review guidelines](doc/development/code_review.md) into account.
-### Merge request description format
-
-Please submit merge requests using the following template in the merge request
-description area. Copy-paste it to retain the markdown format.
-
-```
-## What does this MR do?
-
-## Are there points in the code the reviewer needs to double check?
-
-## Why was this MR needed?
-
-## What are the relevant issue numbers?
-
-## Screenshots (if relevant)
-```
-
### Contribution acceptance criteria
1. The change is as small as possible
@@ -333,8 +324,8 @@ description area. Copy-paste it to retain the markdown format.
aforementioned failing test
1. Your MR initially contains a single commit (please use `git rebase -i` to
squash commits)
-1. Your changes can merge without problems (if not please merge `master`, never
- rebase commits pushed to the remote server)
+1. Your changes can merge without problems (if not please rebase if you're the
+ only one working on your feature branch, otherwise, merge `master`)
1. Does not break any existing functionality
1. Fixes one specific issue or implements one specific feature (do not combine
things, send separate merge requests if needed)
@@ -352,7 +343,10 @@ description area. Copy-paste it to retain the markdown format.
entire line to follow it. This prevents linting tools from generating warnings.
- Don't touch neighbouring lines. As an exception, automatic mass
refactoring modifications may leave style non-compliant.
-1. If the merge request adds any new libraries (gems, JavaScript libraries, etc.), they should conform to our [Licensing guidelines][license-finder-doc]. See the instructions in that document for help if your MR fails the "license-finder" test with a "Dependencies that need approval" error.
+1. If the merge request adds any new libraries (gems, JavaScript libraries,
+ etc.), they should conform to our [Licensing guidelines][license-finder-doc].
+ See the instructions in that document for help if your MR fails the
+ "license-finder" test with a "Dependencies that need approval" error.
## Changes for Stable Releases
@@ -468,7 +462,6 @@ available at [http://contributor-covenant.org/version/1/1/0/](http://contributor
[accepting-mrs-ce]: https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name=Accepting+Merge+Requests
[accepting-mrs-ee]: https://gitlab.com/gitlab-org/gitlab-ee/issues?label_name=Accepting+Merge+Requests
[gitlab-mr-tracker]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests
-[github-mr-tracker]: https://github.com/gitlabhq/gitlabhq/pulls
[gdk]: https://gitlab.com/gitlab-org/gitlab-development-kit
[git-squash]: https://git-scm.com/book/en/Git-Tools-Rewriting-History#Squashing-Commits
[closed-merge-requests]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests?assignee_id=&label_name=&milestone_id=&scope=&sort=&state=closed
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 40c341bdcdb..0f44168a4d5 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-3.6.0
+3.6.4
diff --git a/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 100435be135..b60d71966ae 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-0.8.2
+0.8.4
diff --git a/Gemfile b/Gemfile
index 2ac0f8af755..c5f1ce26daf 100644
--- a/Gemfile
+++ b/Gemfile
@@ -6,10 +6,8 @@ gem 'rails-deprecated_sanitizer', '~> 1.0.3'
# Responders respond_to and respond_with
gem 'responders', '~> 2.0'
-# Specify a sprockets version due to increased performance
-# See https://gitlab.com/gitlab-org/gitlab-ce/issues/6069
-gem 'sprockets', '~> 3.6.0'
-gem 'sprockets-es6'
+gem 'sprockets', '~> 3.7.0'
+gem 'sprockets-es6', '~> 0.9.2'
# Default values for AR models
gem 'default_value_for', '~> 3.0.0'
@@ -19,7 +17,7 @@ gem 'mysql2', '~> 0.3.16', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
# Authentication libraries
-gem 'devise', '~> 4.0'
+gem 'devise', '~> 4.2'
gem 'doorkeeper', '~> 4.2.0'
gem 'omniauth', '~> 1.3.1'
gem 'omniauth-auth0', '~> 1.4.1'
@@ -53,7 +51,7 @@ gem 'browser', '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
-gem 'gitlab_git', '~> 10.6.6'
+gem 'gitlab_git', '~> 10.6.7'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
@@ -101,17 +99,18 @@ gem 'unf', '~> 0.1.4'
gem 'seed-fu', '~> 2.3.5'
# Markdown and HTML processing
-gem 'html-pipeline', '~> 1.11.0'
-gem 'task_list', '~> 1.0.2', require: 'task_list/railtie'
-gem 'github-markup', '~> 1.4'
-gem 'redcarpet', '~> 3.3.3'
-gem 'RedCloth', '~> 4.3.2'
-gem 'rdoc', '~>3.6'
-gem 'org-ruby', '~> 0.9.12'
-gem 'creole', '~> 0.5.0'
-gem 'wikicloth', '0.8.1'
-gem 'asciidoctor', '~> 1.5.2'
-gem 'rouge', '~> 2.0'
+gem 'html-pipeline', '~> 1.11.0'
+gem 'deckar01-task_list', '1.0.5', require: 'task_list/railtie'
+gem 'github-markup', '~> 1.4'
+gem 'redcarpet', '~> 3.3.3'
+gem 'RedCloth', '~> 4.3.2'
+gem 'rdoc', '~>3.6'
+gem 'org-ruby', '~> 0.9.12'
+gem 'creole', '~> 0.5.0'
+gem 'wikicloth', '0.8.1'
+gem 'asciidoctor', '~> 1.5.2'
+gem 'rouge', '~> 2.0'
+gem 'truncato', '~> 0.7.8'
# See https://groups.google.com/forum/#!topic/ruby-security-ann/aSbgDiwb24s
# and https://groups.google.com/forum/#!topic/ruby-security-ann/Dy7YiKb_pMM
@@ -122,8 +121,8 @@ gem 'diffy', '~> 3.0.3'
# Application server
group :unicorn do
- gem 'unicorn', '~> 4.9.0'
- gem 'unicorn-worker-killer', '~> 0.4.2'
+ gem 'unicorn', '~> 5.1.0'
+ gem 'unicorn-worker-killer', '~> 0.4.4'
end
# State machine
@@ -132,7 +131,7 @@ gem 'state_machines-activerecord', '~> 0.4.0'
gem 'after_commit_queue', '~> 1.3.0'
# Issue tags
-gem 'acts-as-taggable-on', '~> 3.4'
+gem 'acts-as-taggable-on', '~> 4.0'
# Background jobs
gem 'sidekiq', '~> 4.2'
@@ -212,7 +211,7 @@ gem 'oj', '~> 2.17.4'
gem 'chronic', '~> 0.10.2'
gem 'chronic_duration', '~> 0.10.6'
-gem 'sass-rails', '~> 5.0.0'
+gem 'sass-rails', '~> 5.0.6'
gem 'coffee-rails', '~> 4.1.0'
gem 'uglifier', '~> 2.7.2'
gem 'turbolinks', '~> 2.5.0'
@@ -233,7 +232,7 @@ gem 'net-ssh', '~> 3.0.1'
gem 'base32', '~> 0.3.0'
# Sentry integration
-gem 'sentry-raven', '~> 1.1.0'
+gem 'sentry-raven', '~> 2.0.0'
gem 'premailer-rails', '~> 1.9.0'
@@ -297,12 +296,11 @@ group :development, :test do
gem 'spring-commands-spinach', '~> 1.1.0'
gem 'spring-commands-teaspoon', '~> 0.0.2'
- gem 'rubocop', '~> 0.42.0', require: false
+ gem 'rubocop', '~> 0.43.0', require: false
gem 'rubocop-rspec', '~> 1.5.0', require: false
gem 'scss_lint', '~> 0.47.0', require: false
gem 'haml_lint', '~> 0.18.2', require: false
gem 'simplecov', '0.12.0', require: false
- gem 'flog', '~> 4.3.2', require: false
gem 'flay', '~> 2.6.1', require: false
gem 'bundler-audit', '~> 0.5.0', require: false
@@ -322,19 +320,15 @@ group :test do
gem 'timecop', '~> 0.8.0'
end
-group :production do
- gem 'gitlab_meta', '7.0'
-end
-
gem 'newrelic_rpm', '~> 3.16'
gem 'octokit', '~> 4.3.0'
-gem 'mail_room', '~> 0.8'
+gem 'mail_room', '~> 0.8.1'
gem 'email_reply_parser', '~> 0.5.8'
-gem 'ruby-prof', '~> 0.15.9'
+gem 'ruby-prof', '~> 0.16.2'
## CI
gem 'activerecord-session_store', '~> 1.0.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index a5a2e5785ac..2feec4c4eb5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -44,8 +44,8 @@ GEM
minitest (~> 5.1)
thread_safe (~> 0.3, >= 0.3.4)
tzinfo (~> 1.1)
- acts-as-taggable-on (3.5.0)
- activerecord (>= 3.2, < 5)
+ acts-as-taggable-on (4.0.0)
+ activerecord (>= 4.0)
addressable (2.3.8)
after_commit_queue (1.3.0)
activerecord (>= 3.0)
@@ -157,11 +157,15 @@ GEM
database_cleaner (1.5.3)
debug_inspector (0.0.2)
debugger-ruby_core_source (1.3.8)
+ deckar01-task_list (1.0.5)
+ activesupport (~> 4.0)
+ html-pipeline
+ rack (~> 1.0)
default_value_for (3.0.2)
activerecord (>= 3.2.0, < 5.1)
descendants_tracker (0.0.4)
thread_safe (~> 0.3, >= 0.3.1)
- devise (4.1.1)
+ devise (4.2.0)
bcrypt (~> 3.0)
orm_adapter (~> 0.1)
railties (>= 4.1.0, < 5.1)
@@ -209,9 +213,6 @@ GEM
flay (2.6.1)
ruby_parser (~> 3.0)
sexp_processor (~> 4.0)
- flog (4.3.2)
- ruby_parser (~> 3.1, > 3.1.0)
- sexp_processor (~> 4.4)
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
@@ -279,12 +280,11 @@ GEM
diff-lcs (~> 1.1)
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
- gitlab_git (10.6.6)
+ gitlab_git (10.6.7)
activesupport (~> 4.0)
charlock_holmes (~> 0.7.3)
github-linguist (~> 4.7.0)
rugged (~> 0.24.0)
- gitlab_meta (7.0)
gitlab_omniauth-ldap (1.2.1)
net-ldap (~> 0.9)
omniauth (~> 1.0)
@@ -399,7 +399,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.8.0)
+ mail_room (0.8.1)
method_source (0.8.2)
mime-types (2.99.3)
mimemagic (0.3.0)
@@ -487,7 +487,7 @@ GEM
orm_adapter (0.5.0)
paranoia (2.1.4)
activerecord (~> 4.0)
- parser (2.3.1.2)
+ parser (2.3.1.4)
ast (~> 2.2)
pg (0.18.4)
pkg-config (1.1.7)
@@ -554,7 +554,7 @@ GEM
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
rainbow (2.1.0)
- raindrops (0.15.0)
+ raindrops (0.17.0)
rake (10.5.0)
rb-fsevent (0.9.6)
rb-inotify (0.9.5)
@@ -588,7 +588,7 @@ GEM
request_store (1.3.1)
rerun (0.11.0)
listen (~> 3.0)
- responders (2.1.1)
+ responders (2.3.0)
railties (>= 4.2.0, < 5.1)
rinku (2.0.0)
rotp (2.1.2)
@@ -620,7 +620,7 @@ GEM
rspec-retry (0.4.5)
rspec-core
rspec-support (3.5.0)
- rubocop (0.42.0)
+ rubocop (0.43.0)
parser (>= 2.3.1.1, < 3.0)
powerpack (~> 0.1)
rainbow (>= 1.99.1, < 3.0)
@@ -630,7 +630,7 @@ GEM
rubocop (>= 0.40.0)
ruby-fogbugz (0.2.1)
crack (~> 0.4)
- ruby-prof (0.15.9)
+ ruby-prof (0.16.2)
ruby-progressbar (1.8.1)
ruby-saml (1.3.0)
nokogiri (>= 1.5.10)
@@ -645,7 +645,7 @@ GEM
sanitize (2.1.0)
nokogiri (>= 1.4.4)
sass (3.4.22)
- sass-rails (5.0.5)
+ sass-rails (5.0.6)
railties (>= 4.0.0, < 6)
sass (~> 3.1)
sprockets (>= 2.8, < 4.0)
@@ -665,8 +665,8 @@ GEM
activesupport (>= 3.1)
select2-rails (3.5.9.3)
thor (~> 0.14)
- sentry-raven (1.1.0)
- faraday (>= 0.7.6)
+ sentry-raven (2.0.2)
+ faraday (>= 0.7.6, < 0.10.x)
settingslogic (2.0.9)
sexp_processor (4.7.0)
sham_rack (1.3.6)
@@ -706,10 +706,10 @@ GEM
spring (>= 0.9.1)
spring-commands-teaspoon (0.0.2)
spring (>= 0.9.1)
- sprockets (3.6.3)
+ sprockets (3.7.0)
concurrent-ruby (~> 1.0)
rack (> 1, < 3)
- sprockets-es6 (0.9.0)
+ sprockets-es6 (0.9.2)
babel-source (>= 5.8.11)
babel-transpiler
sprockets (>= 3.0.0)
@@ -729,8 +729,6 @@ GEM
ffi
sysexits (1.2.0)
systemu (2.6.5)
- task_list (1.0.2)
- html-pipeline
teaspoon (1.1.5)
railties (>= 3.2.5, < 6)
teaspoon-jasmine (2.2.0)
@@ -747,6 +745,9 @@ GEM
tilt (2.0.5)
timecop (0.8.1)
timfel-krb5-auth (0.8.3)
+ truncato (0.7.8)
+ htmlentities (~> 4.3.1)
+ nokogiri (~> 1.6.1)
turbolinks (2.5.3)
coffee-rails
tzinfo (1.2.2)
@@ -760,9 +761,8 @@ GEM
unf_ext
unf_ext (0.0.7.2)
unicode-display_width (1.1.1)
- unicorn (4.9.0)
+ unicorn (5.1.0)
kgio (~> 2.6)
- rack
raindrops (~> 0.7)
unicorn-worker-killer (0.4.4)
get_process_mem (~> 0)
@@ -805,7 +805,7 @@ DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
activerecord-session_store (~> 1.0.0)
- acts-as-taggable-on (~> 3.4)
+ acts-as-taggable-on (~> 4.0)
addressable (~> 2.3.8)
after_commit_queue (~> 1.3.0)
akismet (~> 2.0)
@@ -836,8 +836,9 @@ DEPENDENCIES
creole (~> 0.5.0)
d3_rails (~> 3.5.0)
database_cleaner (~> 1.5.0)
+ deckar01-task_list (= 1.0.5)
default_value_for (~> 3.0.0)
- devise (~> 4.0)
+ devise (~> 4.2)
devise-two-factor (~> 3.0.0)
diffy (~> 3.0.3)
doorkeeper (~> 4.2.0)
@@ -847,7 +848,6 @@ DEPENDENCIES
factory_girl_rails (~> 4.6.0)
ffaker (~> 2.0.0)
flay (~> 2.6.1)
- flog (~> 4.3.2)
fog-aws (~> 0.9)
fog-azure (~> 0.0)
fog-core (~> 1.40)
@@ -863,8 +863,7 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
github-markup (~> 1.4)
gitlab-flowdock-git-hook (~> 1.0.1)
- gitlab_git (~> 10.6.6)
- gitlab_meta (= 7.0)
+ gitlab_git (~> 10.6.7)
gitlab_omniauth-ldap (~> 1.2.1)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.2)
@@ -890,7 +889,7 @@ DEPENDENCIES
license_finder (~> 2.1.0)
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.8)
+ mail_room (~> 0.8.1)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
@@ -942,17 +941,17 @@ DEPENDENCIES
rqrcode-rails3 (~> 0.1.7)
rspec-rails (~> 3.5.0)
rspec-retry (~> 0.4.5)
- rubocop (~> 0.42.0)
+ rubocop (~> 0.43.0)
rubocop-rspec (~> 1.5.0)
ruby-fogbugz (~> 0.2.1)
- ruby-prof (~> 0.15.9)
+ ruby-prof (~> 0.16.2)
sanitize (~> 2.0)
- sass-rails (~> 5.0.0)
+ sass-rails (~> 5.0.6)
scss_lint (~> 0.47.0)
sdoc (~> 0.3.20)
seed-fu (~> 2.3.5)
select2-rails (~> 3.5.9)
- sentry-raven (~> 1.1.0)
+ sentry-raven (~> 2.0.0)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
shoulda-matchers (~> 2.8.0)
@@ -966,23 +965,23 @@ DEPENDENCIES
spring-commands-rspec (~> 1.0.4)
spring-commands-spinach (~> 1.1.0)
spring-commands-teaspoon (~> 0.0.2)
- sprockets (~> 3.6.0)
- sprockets-es6
+ sprockets (~> 3.7.0)
+ sprockets-es6 (~> 0.9.2)
state_machines-activerecord (~> 0.4.0)
sys-filesystem (~> 1.1.6)
- task_list (~> 1.0.2)
teaspoon (~> 1.1.0)
teaspoon-jasmine (~> 2.2.0)
test_after_commit (~> 0.4.2)
thin (~> 1.7.0)
timecop (~> 0.8.0)
+ truncato (~> 0.7.8)
turbolinks (~> 2.5.0)
u2f (~> 0.2.1)
uglifier (~> 2.7.2)
underscore-rails (~> 1.8.0)
unf (~> 0.1.4)
- unicorn (~> 4.9.0)
- unicorn-worker-killer (~> 0.4.2)
+ unicorn (~> 5.1.0)
+ unicorn-worker-killer (~> 0.4.4)
version_sorter (~> 2.1.0)
virtus (~> 1.0.1)
vmstat (~> 2.2)
@@ -991,4 +990,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.13.0
+ 1.13.2
diff --git a/README.md b/README.md
index 9661a554b9f..a6b30aff5a0 100644
--- a/README.md
+++ b/README.md
@@ -1,13 +1,13 @@
# GitLab
-[![build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
-[![coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+[![Build status](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/build.svg)](https://gitlab.com/gitlab-org/gitlab-ce/commits/master)
+[![CE coverage report](https://gitlab.com/gitlab-org/gitlab-ce/badges/master/coverage.svg?job=coverage)](http://gitlab-org.gitlab.io/gitlab-ce/coverage-ruby)
[![Code Climate](https://codeclimate.com/github/gitlabhq/gitlabhq.svg)](https://codeclimate.com/github/gitlabhq/gitlabhq)
[![Core Infrastructure Initiative Best Practices](https://bestpractices.coreinfrastructure.org/projects/42/badge)](https://bestpractices.coreinfrastructure.org/projects/42)
## Canonical source
-The cannonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
+The canonical source of GitLab Community Edition is [hosted on GitLab.com](https://gitlab.com/gitlab-org/gitlab-ce/).
## Open source software to collaborate on code
diff --git a/app/assets/javascripts/LabelManager.js b/app/assets/javascripts/LabelManager.js
deleted file mode 100644
index d4a4c7abaa1..00000000000
--- a/app/assets/javascripts/LabelManager.js
+++ /dev/null
@@ -1,115 +0,0 @@
-(function() {
- this.LabelManager = (function() {
- LabelManager.prototype.errorMessage = 'Unable to update label prioritization at this time';
-
- function LabelManager(opts) {
- // Defaults
- var ref, ref1, ref2;
- if (opts == null) {
- opts = {};
- }
- this.togglePriorityButton = (ref = opts.togglePriorityButton) != null ? ref : $('.js-toggle-priority'), this.prioritizedLabels = (ref1 = opts.prioritizedLabels) != null ? ref1 : $('.js-prioritized-labels'), this.otherLabels = (ref2 = opts.otherLabels) != null ? ref2 : $('.js-other-labels');
- this.prioritizedLabels.sortable({
- items: 'li',
- placeholder: 'list-placeholder',
- axis: 'y',
- update: this.onPrioritySortUpdate.bind(this)
- });
- this.bindEvents();
- }
-
- LabelManager.prototype.bindEvents = function() {
- return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
- };
-
- LabelManager.prototype.onTogglePriorityClick = function(e) {
- var $btn, $label, $tooltip, _this, action;
- e.preventDefault();
- _this = e.data;
- $btn = $(e.currentTarget);
- $label = $("#" + ($btn.data('domId')));
- action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
- // Make sure tooltip will hide
- $tooltip = $("#" + ($btn.find('.has-tooltip:visible').attr('aria-describedby')));
- $tooltip.tooltip('destroy');
- return _this.toggleLabelPriority($label, action);
- };
-
- LabelManager.prototype.toggleLabelPriority = function($label, action, persistState) {
- var $from, $target, _this, url, xhr;
- if (persistState == null) {
- persistState = true;
- }
- _this = this;
- url = $label.find('.js-toggle-priority').data('url');
- $target = this.prioritizedLabels;
- $from = this.otherLabels;
- // Optimistic update
- if (action === 'remove') {
- $target = this.otherLabels;
- $from = this.prioritizedLabels;
- }
- if ($from.find('li').length === 1) {
- $from.find('.empty-message').removeClass('hidden');
- }
- if (!$target.find('li').length) {
- $target.find('.empty-message').addClass('hidden');
- }
- $label.detach().appendTo($target);
- // Return if we are not persisting state
- if (!persistState) {
- return;
- }
- if (action === 'remove') {
- xhr = $.ajax({
- url: url,
- type: 'DELETE'
- });
- // Restore empty message
- if (!$from.find('li').length) {
- $from.find('.empty-message').removeClass('hidden');
- }
- } else {
- xhr = this.savePrioritySort($label, action);
- }
- return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
- };
-
- LabelManager.prototype.onPrioritySortUpdate = function() {
- var xhr;
- xhr = this.savePrioritySort();
- return xhr.fail(function() {
- return new Flash(this.errorMessage, 'alert');
- });
- };
-
- LabelManager.prototype.savePrioritySort = function() {
- return $.post({
- url: this.prioritizedLabels.data('url'),
- data: {
- label_ids: this.getSortedLabelsIds()
- }
- });
- };
-
- LabelManager.prototype.rollbackLabelPosition = function($label, originalAction) {
- var action;
- action = originalAction === 'remove' ? 'add' : 'remove';
- this.toggleLabelPriority($label, action, false);
- return new Flash(this.errorMessage, 'alert');
- };
-
- LabelManager.prototype.getSortedLabelsIds = function() {
- var sortedIds;
- sortedIds = [];
- this.prioritizedLabels.find('li').each(function() {
- return sortedIds.push($(this).data('id'));
- });
- return sortedIds;
- };
-
- return LabelManager;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/activities.js b/app/assets/javascripts/activities.js
index d5e11e22be5..f4f8cf04184 100644
--- a/app/assets/javascripts/activities.js
+++ b/app/assets/javascripts/activities.js
@@ -21,16 +21,14 @@
};
Activities.prototype.toggleFilter = function(sender) {
- var event_filters, filter;
+ var filter = sender.attr("id").split("_")[0];
+
$('.event-filter .active').removeClass("active");
- event_filters = $.cookie("event_filter");
- filter = sender.attr("id").split("_")[0];
- $.cookie("event_filter", (event_filters !== filter ? filter : ""), {
+ $.cookie("event_filter", filter, {
path: gon.relative_url_root || '/'
});
- if (event_filters !== filter) {
- return sender.closest('li').toggleClass("active");
- }
+
+ sender.closest('li').toggleClass("active");
};
return Activities;
diff --git a/app/assets/javascripts/api.js b/app/assets/javascripts/api.js
index 1cd2302111e..599331df3f5 100644
--- a/app/assets/javascripts/api.js
+++ b/app/assets/javascripts/api.js
@@ -5,7 +5,7 @@
namespacesPath: "/api/:version/namespaces.json",
groupProjectsPath: "/api/:version/groups/:id/projects.json",
projectsPath: "/api/:version/projects.json?simple=true",
- labelsPath: "/api/:version/projects/:id/labels",
+ labelsPath: "/:namespace_path/:project_path/labels",
licensePath: "/api/:version/licenses/:key",
gitignorePath: "/api/:version/gitignores/:key",
gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key",
@@ -23,12 +23,13 @@
},
// Return groups list. Filtered by query
// Only active groups retrieved
- groups: function(query, skip_ldap, callback) {
+ groups: function(query, skip_ldap, skip_groups, callback) {
var url = Api.buildUrl(Api.groupsPath);
return $.ajax({
url: url,
data: {
search: query,
+ skip_groups: skip_groups,
per_page: 20
},
dataType: "json"
@@ -65,13 +66,14 @@
return callback(projects);
});
},
- newLabel: function(project_id, data, callback) {
+ newLabel: function(namespace_path, project_path, data, callback) {
var url = Api.buildUrl(Api.labelsPath)
- .replace(':id', project_id);
+ .replace(':namespace_path', namespace_path)
+ .replace(':project_path', project_path);
return $.ajax({
url: url,
type: "POST",
- data: data,
+ data: {'label': data},
dataType: "json"
}).done(function(label) {
return callback(label);
diff --git a/app/assets/javascripts/application.js b/app/assets/javascripts/application.js
index c029bf3b5ca..8a61669822c 100644
--- a/app/assets/javascripts/application.js
+++ b/app/assets/javascripts/application.js
@@ -247,7 +247,7 @@
$this.toggleClass('active');
var notesHolders = $this.closest('.diff-file').find('.notes_holder');
if ($this.hasClass('active')) {
- notesHolders.show();
+ notesHolders.show().find('.hide').show();
} else {
notesHolders.hide();
}
diff --git a/app/assets/javascripts/awards_handler.js b/app/assets/javascripts/awards_handler.js
index 0decc6d09e6..44af1c135a0 100644
--- a/app/assets/javascripts/awards_handler.js
+++ b/app/assets/javascripts/awards_handler.js
@@ -357,7 +357,7 @@
$('ul.emoji-menu-search, h5.emoji-search').remove();
if (term) {
// Generate a search result block
- h5 = $('<h5>').text('Search results');
+ h5 = $('<h5 class="emoji-search" />').text('Search results');
found_emojis = _this.searchEmojis(term).show();
ul = $('<ul>').addClass('emoji-menu-list emoji-menu-search').append(found_emojis);
$('.emoji-menu-content ul, .emoji-menu-content h5').hide();
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js b/app/assets/javascripts/blob/blob_ci_yaml.js
deleted file mode 100644
index 68758574967..00000000000
--- a/app/assets/javascripts/blob/blob_ci_yaml.js
+++ /dev/null
@@ -1,46 +0,0 @@
-
-/*= require blob/template_selector */
-
-(function() {
- var extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
- hasProp = {}.hasOwnProperty;
-
- this.BlobCiYamlSelector = (function(superClass) {
- extend(BlobCiYamlSelector, superClass);
-
- function BlobCiYamlSelector() {
- return BlobCiYamlSelector.__super__.constructor.apply(this, arguments);
- }
-
- BlobCiYamlSelector.prototype.requestFile = function(query) {
- return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
- };
-
- return BlobCiYamlSelector;
-
- })(TemplateSelector);
-
- this.BlobCiYamlSelectors = (function() {
- function BlobCiYamlSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-gitlab-ci-yml-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobCiYamlSelector({
- pattern: /(.gitlab-ci.yml)/,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobCiYamlSelectors;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.es6 b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
new file mode 100644
index 00000000000..d6ea4f84f57
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.es6
@@ -0,0 +1,40 @@
+/*= require blob/template_selector */
+((global) => {
+
+ class BlobCiYamlSelector extends gl.TemplateSelector {
+ requestFile(query) {
+ return Api.gitlabCiYml(query.name, this.requestFileSuccess.bind(this));
+ }
+
+ requestFileSuccess(file) {
+ return super.requestFileSuccess(file);
+ }
+ }
+
+ global.BlobCiYamlSelector = BlobCiYamlSelector;
+
+ class BlobCiYamlSelectors {
+ constructor({ editor, $dropdowns } = {}) {
+ this.editor = editor;
+ this.$dropdowns = $dropdowns || $('.js-gitlab-ci-yml-selector');
+ this.initSelectors();
+ }
+
+ initSelectors() {
+ const editor = this.editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobCiYamlSelector({
+ editor,
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown
+ });
+ });
+ }
+ }
+
+ global.BlobCiYamlSelectors = BlobCiYamlSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/blob_gitignore_selector.js b/app/assets/javascripts/blob/blob_gitignore_selector.js
index 54a09e919f8..cd746b05cf6 100644
--- a/app/assets/javascripts/blob/blob_gitignore_selector.js
+++ b/app/assets/javascripts/blob/blob_gitignore_selector.js
@@ -18,6 +18,6 @@
return BlobGitignoreSelector;
- })(TemplateSelector);
+ })(gl.TemplateSelector);
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selector.js b/app/assets/javascripts/blob/blob_license_selector.js
index 9a8ef08f4e5..2701df3e6de 100644
--- a/app/assets/javascripts/blob/blob_license_selector.js
+++ b/app/assets/javascripts/blob/blob_license_selector.js
@@ -23,6 +23,6 @@
return BlobLicenseSelector;
- })(TemplateSelector);
+ })(gl.TemplateSelector);
}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js b/app/assets/javascripts/blob/blob_license_selectors.js
deleted file mode 100644
index 39237705e8d..00000000000
--- a/app/assets/javascripts/blob/blob_license_selectors.js
+++ /dev/null
@@ -1,25 +0,0 @@
-(function() {
- this.BlobLicenseSelectors = (function() {
- function BlobLicenseSelectors(opts) {
- var ref;
- this.$dropdowns = (ref = opts.$dropdowns) != null ? ref : $('.js-license-selector'), this.editor = opts.editor;
- this.$dropdowns.each((function(_this) {
- return function(i, dropdown) {
- var $dropdown;
- $dropdown = $(dropdown);
- return new BlobLicenseSelector({
- pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
- data: $dropdown.data('data'),
- wrapper: $dropdown.closest('.js-license-selector-wrap'),
- dropdown: $dropdown,
- editor: _this.editor
- });
- };
- })(this));
- }
-
- return BlobLicenseSelectors;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/blob_license_selectors.js.es6 b/app/assets/javascripts/blob/blob_license_selectors.js.es6
new file mode 100644
index 00000000000..153ed457559
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_license_selectors.js.es6
@@ -0,0 +1,21 @@
+((global) => {
+ class BlobLicenseSelectors {
+ constructor({ $dropdowns, editor }) {
+ this.$dropdowns = $('.js-license-selector');
+ this.editor = editor;
+ this.$dropdowns.each((i, dropdown) => {
+ const $dropdown = $(dropdown);
+ return new BlobLicenseSelector({
+ editor,
+ pattern: /^(.+\/)?(licen[sc]e|copying)($|\.)/i,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-license-selector-wrap'),
+ dropdown: $dropdown,
+ });
+ });
+ }
+ }
+
+ global.BlobLicenseSelectors = BlobLicenseSelectors;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/blob/template_selector.js b/app/assets/javascripts/blob/template_selector.js
deleted file mode 100644
index 95352164d76..00000000000
--- a/app/assets/javascripts/blob/template_selector.js
+++ /dev/null
@@ -1,100 +0,0 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.TemplateSelector = (function() {
- function TemplateSelector(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.onClick = bind(this.onClick, this);
- this.dropdown = opts.dropdown, this.data = opts.data, this.pattern = opts.pattern, this.wrapper = opts.wrapper, this.editor = opts.editor, this.fileEndpoint = opts.fileEndpoint, this.$input = (ref = opts.$input) != null ? ref : $('#file_name');
- this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
- this.buildDropdown();
- this.bindEvents();
- this.onFilenameUpdate();
-
- this.autosizeUpdateEvent = document.createEvent('Event');
- this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
- }
-
- TemplateSelector.prototype.buildDropdown = function() {
- return this.dropdown.glDropdown({
- data: this.data,
- filterable: true,
- selectable: true,
- toggleLabel: this.toggleLabel,
- search: {
- fields: ['name']
- },
- clicked: this.onClick,
- text: function(item) {
- return item.name;
- }
- });
- };
-
- TemplateSelector.prototype.bindEvents = function() {
- return this.$input.on('keyup blur', (function(_this) {
- return function(e) {
- return _this.onFilenameUpdate();
- };
- })(this));
- };
-
- TemplateSelector.prototype.toggleLabel = function(item) {
- return item.name;
- };
-
- TemplateSelector.prototype.onFilenameUpdate = function() {
- var filenameMatches;
- if (!this.$input.length) {
- return;
- }
- filenameMatches = this.pattern.test(this.$input.val().trim());
- if (!filenameMatches) {
- this.wrapper.addClass('hidden');
- return;
- }
- return this.wrapper.removeClass('hidden');
- };
-
- TemplateSelector.prototype.onClick = function(item, el, e) {
- e.preventDefault();
- return this.requestFile(item);
- };
-
- TemplateSelector.prototype.requestFile = function(item) {
- // This `requestFile` method is an abstract method that should
- // be added by all subclasses.
- };
-
- // To be implemented on the extending class
- // e.g.
- // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
- TemplateSelector.prototype.requestFileSuccess = function(file, skipFocus) {
- this.editor.setValue(file.content, 1);
- if (!skipFocus) this.editor.focus();
-
- if (this.editor instanceof jQuery) {
- this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
- }
- };
-
- TemplateSelector.prototype.startLoadingSpinner = function() {
- this.dropdownIcon
- .addClass('fa-spinner fa-spin')
- .removeClass('fa-chevron-down');
- };
-
- TemplateSelector.prototype.stopLoadingSpinner = function() {
- this.dropdownIcon
- .addClass('fa-chevron-down')
- .removeClass('fa-spinner fa-spin');
- };
-
- return TemplateSelector;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/blob/template_selector.js.es6 b/app/assets/javascripts/blob/template_selector.js.es6
new file mode 100644
index 00000000000..4e309e480b0
--- /dev/null
+++ b/app/assets/javascripts/blob/template_selector.js.es6
@@ -0,0 +1,102 @@
+((global) => {
+ class TemplateSelector {
+ constructor({ dropdown, data, pattern, wrapper, editor, fileEndpoint, $input } = {}) {
+ this.onClick = this.onClick.bind(this);
+ this.dropdown = dropdown;
+ this.data = data;
+ this.pattern = pattern;
+ this.wrapper = wrapper;
+ this.editor = editor;
+ this.fileEndpoint = fileEndpoint;
+ this.$input = $input || $('#file_name');
+ this.dropdownIcon = $('.fa-chevron-down', this.dropdown);
+ this.buildDropdown();
+ this.bindEvents();
+ this.onFilenameUpdate();
+
+ this.autosizeUpdateEvent = document.createEvent('Event');
+ this.autosizeUpdateEvent.initEvent('autosize:update', true, false);
+ }
+
+ buildDropdown() {
+ return this.dropdown.glDropdown({
+ data: this.data,
+ filterable: true,
+ selectable: true,
+ toggleLabel: this.toggleLabel,
+ search: {
+ fields: ['name']
+ },
+ clicked: this.onClick,
+ text: function(item) {
+ return item.name;
+ }
+ });
+ }
+
+ bindEvents() {
+ return this.$input.on('keyup blur', (e) => this.onFilenameUpdate());
+ }
+
+ toggleLabel(item) {
+ return item.name;
+ }
+
+ onFilenameUpdate() {
+ var filenameMatches;
+ if (!this.$input.length) {
+ return;
+ }
+ filenameMatches = this.pattern.test(this.$input.val().trim());
+ if (!filenameMatches) {
+ this.wrapper.addClass('hidden');
+ return;
+ }
+ return this.wrapper.removeClass('hidden');
+ }
+
+ onClick(item, el, e) {
+ e.preventDefault();
+ return this.requestFile(item);
+ }
+
+ requestFile(item) {
+ // This `requestFile` method is an abstract method that should
+ // be added by all subclasses.
+ }
+
+ // To be implemented on the extending class
+ // e.g.
+ // Api.gitignoreText item.name, @requestFileSuccess.bind(@)
+ requestFileSuccess(file, { skipFocus, append } = {}) {
+ const oldValue = this.editor.getValue();
+ let newValue = file.content;
+
+ if (append && oldValue.length && oldValue !== newValue) {
+ newValue = oldValue + '\n\n' + newValue;
+ }
+
+ this.editor.setValue(newValue, 1);
+ if (!skipFocus) this.editor.focus();
+
+ if (this.editor instanceof jQuery) {
+ this.editor.get(0).dispatchEvent(this.autosizeUpdateEvent);
+ }
+ }
+
+ startLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-spinner fa-spin')
+ .removeClass('fa-chevron-down');
+ }
+
+ stopLoadingSpinner() {
+ this.dropdownIcon
+ .addClass('fa-chevron-down')
+ .removeClass('fa-spinner fa-spin');
+ }
+ }
+
+ global.TemplateSelector = TemplateSelector;
+ })(window.gl || ( window.gl = {}));
+
diff --git a/app/assets/javascripts/blob_edit/edit_blob.js b/app/assets/javascripts/blob_edit/edit_blob.js
index b846bab0424..8db4f6a3b28 100644
--- a/app/assets/javascripts/blob_edit/edit_blob.js
+++ b/app/assets/javascripts/blob_edit/edit_blob.js
@@ -22,13 +22,14 @@
// submitted textarea
})(this));
this.initModePanesAndLinks();
- new BlobLicenseSelectors({
+ this.initSoftWrap();
+ new gl.BlobLicenseSelectors({
editor: this.editor
});
new BlobGitignoreSelectors({
editor: this.editor
});
- new BlobCiYamlSelectors({
+ new gl.BlobCiYamlSelectors({
editor: this.editor
});
}
@@ -50,6 +51,7 @@
this.$editModePanes.hide();
currentPane.fadeIn(200);
if (paneId === "#preview") {
+ this.$toggleButton.hide();
return $.post(currentLink.data("preview-url"), {
content: this.editor.getValue()
}, function(response) {
@@ -57,10 +59,23 @@
return currentPane.syntaxHighlight();
});
} else {
+ this.$toggleButton.show();
return this.editor.focus();
}
};
+ EditBlob.prototype.initSoftWrap = function() {
+ this.isSoftWrapped = false;
+ this.$toggleButton = $('.soft-wrap-toggle');
+ this.$toggleButton.on('click', this.toggleSoftWrap.bind(this));
+ };
+
+ EditBlob.prototype.toggleSoftWrap = function(e) {
+ this.isSoftWrapped = !this.isSoftWrapped;
+ this.$toggleButton.toggleClass('soft-wrap-active', this.isSoftWrapped);
+ this.editor.getSession().setUseWrapMode(this.isSoftWrapped);
+ };
+
return EditBlob;
})();
diff --git a/app/assets/javascripts/boards/components/board.js.es6 b/app/assets/javascripts/boards/components/board.js.es6
index 7e86f001f44..cacb36a897f 100644
--- a/app/assets/javascripts/boards/components/board.js.es6
+++ b/app/assets/javascripts/boards/components/board.js.es6
@@ -21,7 +21,8 @@
},
data () {
return {
- filters: Store.state.filters
+ filters: Store.state.filters,
+ showIssueForm: false
};
},
watch: {
@@ -33,6 +34,11 @@
deep: true
}
},
+ methods: {
+ showNewIssueForm() {
+ this.showIssueForm = !this.showIssueForm;
+ }
+ },
ready () {
const options = gl.issueBoards.getBoardSortableDefaultOptions({
disabled: this.disabled,
diff --git a/app/assets/javascripts/boards/components/board_blank_state.js.es6 b/app/assets/javascripts/boards/components/board_blank_state.js.es6
index 63d72d857d9..ff90f2d6d75 100644
--- a/app/assets/javascripts/boards/components/board_blank_state.js.es6
+++ b/app/assets/javascripts/boards/components/board_blank_state.js.es6
@@ -8,10 +8,8 @@
data () {
return {
predefinedLabels: [
- new ListLabel({ title: 'Development', color: '#5CB85C' }),
- new ListLabel({ title: 'Testing', color: '#F0AD4E' }),
- new ListLabel({ title: 'Production', color: '#FF5F00' }),
- new ListLabel({ title: 'Ready', color: '#FF0000' })
+ new ListLabel({ title: 'To Do', color: '#F0AD4E' }),
+ new ListLabel({ title: 'Doing', color: '#5CB85C' })
]
}
},
diff --git a/app/assets/javascripts/boards/components/board_list.js.es6 b/app/assets/javascripts/boards/components/board_list.js.es6
index 474805c1437..7022a29e818 100644
--- a/app/assets/javascripts/boards/components/board_list.js.es6
+++ b/app/assets/javascripts/boards/components/board_list.js.es6
@@ -1,4 +1,5 @@
//= require ./board_card
+//= require ./board_new_issue
(() => {
const Store = gl.issueBoards.BoardsStore;
@@ -8,14 +9,16 @@
gl.issueBoards.BoardList = Vue.extend({
components: {
- 'board-card': gl.issueBoards.BoardCard
+ 'board-card': gl.issueBoards.BoardCard,
+ 'board-new-issue': gl.issueBoards.BoardNewIssue
},
props: {
disabled: Boolean,
list: Object,
issues: Array,
loading: Boolean,
- issueLinkBase: String
+ issueLinkBase: String,
+ showIssueForm: Boolean
},
data () {
return {
@@ -73,7 +76,7 @@
group: 'issues',
sort: false,
disabled: this.disabled,
- filter: '.board-list-count',
+ filter: '.board-list-count, .is-disabled',
onStart: (e) => {
const card = this.$refs.issue[e.oldIndex];
diff --git a/app/assets/javascripts/boards/components/board_new_issue.js.es6 b/app/assets/javascripts/boards/components/board_new_issue.js.es6
new file mode 100644
index 00000000000..a4fad422eca
--- /dev/null
+++ b/app/assets/javascripts/boards/components/board_new_issue.js.es6
@@ -0,0 +1,58 @@
+(() => {
+ window.gl = window.gl || {};
+
+ gl.issueBoards.BoardNewIssue = Vue.extend({
+ props: {
+ list: Object,
+ showIssueForm: Boolean
+ },
+ data() {
+ return {
+ title: '',
+ error: false
+ };
+ },
+ watch: {
+ showIssueForm () {
+ this.$els.input.focus();
+ }
+ },
+ methods: {
+ submit(e) {
+ e.preventDefault();
+ if (this.title.trim() === '') return;
+
+ this.error = false;
+
+ const labels = this.list.label ? [this.list.label] : [];
+ const issue = new ListIssue({
+ title: this.title,
+ labels
+ });
+
+ this.list.newIssue(issue)
+ .then((data) => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$els.submitButton).enable();
+ })
+ .catch(() => {
+ // Need this because our jQuery very kindly disables buttons on ALL form submissions
+ $(this.$els.submitButton).enable();
+
+ // Remove the issue
+ this.list.removeIssue(issue);
+
+ // Show error message
+ this.error = true;
+ this.showIssueForm = true;
+ });
+
+ this.cancel();
+ },
+ cancel() {
+ this.showIssueForm = false;
+ this.title = '';
+ }
+ }
+ });
+})();
diff --git a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6 b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
index 1a4d8157970..6ccd83e2d84 100644
--- a/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
+++ b/app/assets/javascripts/boards/components/new_list_dropdown.js.es6
@@ -3,8 +3,7 @@ $(() => {
$('.js-new-board-list').each(function () {
const $this = $(this);
-
- new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('project-id'));
+ new gl.CreateLabelDropdown($this.closest('.dropdown').find('.dropdown-new-label'), $this.data('namespace-path'), $this.data('project-path'));
$this.glDropdown({
data(term, callback) {
diff --git a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6 b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
index 44addb3ea98..f629d45c587 100644
--- a/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
+++ b/app/assets/javascripts/boards/mixins/sortable_default_options.js.es6
@@ -21,7 +21,7 @@
fallbackClass: 'is-dragging',
fallbackOnBody: true,
ghostClass: 'is-ghost',
- filter: '.has-tooltip',
+ filter: '.has-tooltip, .btn',
delay: gl.issueBoards.touchEnabled ? 100 : 0,
scrollSensitivity: gl.issueBoards.touchEnabled ? 60 : 100,
scrollSpeed: 20,
diff --git a/app/assets/javascripts/boards/models/list.js.es6 b/app/assets/javascripts/boards/models/list.js.es6
index 91fd620fdb3..5d0a561cdba 100644
--- a/app/assets/javascripts/boards/models/list.js.es6
+++ b/app/assets/javascripts/boards/models/list.js.es6
@@ -87,6 +87,17 @@ class List {
});
}
+ newIssue (issue) {
+ this.addIssue(issue);
+ this.issuesSize++;
+
+ return gl.boardService.newIssue(this.id, issue)
+ .then((resp) => {
+ const data = resp.json();
+ issue.id = data.iid;
+ });
+ }
+
createIssues (data) {
data.forEach((issueObj) => {
this.addIssue(new ListIssue(issueObj));
diff --git a/app/assets/javascripts/boards/services/board_service.js.es6 b/app/assets/javascripts/boards/services/board_service.js.es6
index 9b80fb2e99f..2b825c3949f 100644
--- a/app/assets/javascripts/boards/services/board_service.js.es6
+++ b/app/assets/javascripts/boards/services/board_service.js.es6
@@ -58,4 +58,10 @@ class BoardService {
to_list_id
});
}
+
+ newIssue (id, issue) {
+ return this.issues.save({ id }, {
+ issue
+ });
+ }
};
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 78d21c0552a..f336bfc36d6 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -146,7 +146,7 @@
$date = $('.js-artifacts-remove');
if ($date.length) {
date = $date.text();
- return $date.text($.timefor(new Date(date.replace(/-/g, '/')), ' '));
+ return $date.text($.timefor(new Date(date.replace(/([0-9]+)-([0-9]+)-([0-9]+)/g, '$1/$2/$3')), ' '));
}
};
diff --git a/app/assets/javascripts/commit/image-file.js b/app/assets/javascripts/commit/image_file.js
index e893491b19b..e893491b19b 100644
--- a/app/assets/javascripts/commit/image-file.js
+++ b/app/assets/javascripts/commit/image_file.js
diff --git a/app/assets/javascripts/compare_autocomplete.js b/app/assets/javascripts/compare_autocomplete.js
index 4e3a28cd163..294d2c9052c 100644
--- a/app/assets/javascripts/compare_autocomplete.js
+++ b/app/assets/javascripts/compare_autocomplete.js
@@ -23,8 +23,9 @@
selectable: true,
filterable: true,
filterByText: true,
- fieldName: $dropdown.attr('name'),
- filterInput: 'input[type="text"]',
+ toggleLabel: true,
+ fieldName: $dropdown.data('field-name'),
+ filterInput: 'input[type="search"]',
renderRow: function(ref) {
var link;
if (ref.header != null) {
diff --git a/app/assets/javascripts/copy_to_clipboard.js b/app/assets/javascripts/copy_to_clipboard.js
index 3e20db7e308..e23bda2fa4e 100644
--- a/app/assets/javascripts/copy_to_clipboard.js
+++ b/app/assets/javascripts/copy_to_clipboard.js
@@ -26,15 +26,15 @@
};
showTooltip = function(target, title) {
- return $(target).tooltip({
- container: 'body',
- html: 'true',
- placement: 'auto bottom',
- title: title,
- trigger: 'manual'
- }).tooltip('show').one('mouseleave', function() {
- return $(this).tooltip('hide');
- });
+ var $target = $(target);
+ var originalTitle = $target.data('original-title');
+
+ $target
+ .attr('title', 'Copied!')
+ .tooltip('fixTitle')
+ .tooltip('show')
+ .attr('title', originalTitle)
+ .tooltip('fixTitle');
};
$(function() {
diff --git a/app/assets/javascripts/create_label.js.es6 b/app/assets/javascripts/create_label.js.es6
index 46d1c3f00c1..c5f8c29242d 100644
--- a/app/assets/javascripts/create_label.js.es6
+++ b/app/assets/javascripts/create_label.js.es6
@@ -1,8 +1,9 @@
(function (w) {
class CreateLabelDropdown {
- constructor ($el, projectId) {
+ constructor ($el, namespacePath, projectPath) {
this.$el = $el;
- this.projectId = projectId;
+ this.namespacePath = namespacePath;
+ this.projectPath = projectPath;
this.$dropdownBack = $('.dropdown-menu-back', this.$el.closest('.dropdown'));
this.$cancelButton = $('.js-cancel-label-btn', this.$el);
this.$newLabelField = $('#new_label_name', this.$el);
@@ -91,8 +92,8 @@
e.preventDefault();
e.stopPropagation();
- Api.newLabel(this.projectId, {
- name: this.$newLabelField.val(),
+ Api.newLabel(this.namespacePath, this.projectPath, {
+ title: this.$newLabelField.val(),
color: this.$newColorField.val()
}, (label) => {
this.$newLabelCreateButton.enable();
diff --git a/app/assets/javascripts/cycle-analytics.js.es6 b/app/assets/javascripts/cycle_analytics.js.es6
index cd9886ba58d..cd9886ba58d 100644
--- a/app/assets/javascripts/cycle-analytics.js.es6
+++ b/app/assets/javascripts/cycle_analytics.js.es6
diff --git a/app/assets/javascripts/diff.js b/app/assets/javascripts/diff.js
index c8634b78f2b..8086c10ad6b 100644
--- a/app/assets/javascripts/diff.js
+++ b/app/assets/javascripts/diff.js
@@ -7,6 +7,9 @@
function Diff() {
$('.files .diff-file').singleFileDiff();
this.filesCommentButton = $('.files .diff-file').filesCommentButton();
+ if (this.diffViewType() === 'parallel') {
+ $('.content-wrapper .container-fluid').removeClass('container-limited');
+ }
$(document).off('click', '.js-unfold');
$(document).on('click', '.js-unfold', (function(_this) {
return function(event) {
@@ -52,6 +55,10 @@
})(this));
}
+ Diff.prototype.diffViewType = function() {
+ return $('.inline-parallel-buttons a.active').data('view-type');
+ }
+
Diff.prototype.lineNumbers = function(line) {
if (!line.children().length) {
return [0, 0];
diff --git a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
index be6ebc77947..cdedfd1af15 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_btn.js.es6
@@ -1,13 +1,9 @@
((w) => {
w.ResolveBtn = Vue.extend({
- mixins: [
- ButtonMixins
- ],
props: {
noteId: Number,
discussionId: String,
resolved: Boolean,
- namespacePath: String,
projectPath: String,
canResolve: Boolean,
resolvedBy: String
@@ -69,10 +65,10 @@
if (this.isResolved) {
promise = ResolveService
- .unresolve(this.namespace, this.noteId);
+ .unresolve(this.projectPath, this.noteId);
} else {
promise = ResolveService
- .resolve(this.namespace, this.noteId);
+ .resolve(this.projectPath, this.noteId);
}
promise.then((response) => {
diff --git a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6 b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
index e373b06b1eb..0a617034502 100644
--- a/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
+++ b/app/assets/javascripts/diff_notes/components/resolve_discussion_btn.js.es6
@@ -1,12 +1,8 @@
((w) => {
w.ResolveDiscussionBtn = Vue.extend({
- mixins: [
- ButtonMixins
- ],
props: {
discussionId: String,
mergeRequestId: Number,
- namespacePath: String,
projectPath: String,
canResolve: Boolean,
},
@@ -50,7 +46,7 @@
},
methods: {
resolve: function () {
- ResolveService.toggleResolveForDiscussion(this.namespace, this.mergeRequestId, this.discussionId);
+ ResolveService.toggleResolveForDiscussion(this.projectPath, this.mergeRequestId, this.discussionId);
}
},
created: function () {
diff --git a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6 b/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
deleted file mode 100644
index d278678085b..00000000000
--- a/app/assets/javascripts/diff_notes/mixins/namespace.js.es6
+++ /dev/null
@@ -1,9 +0,0 @@
-((w) => {
- w.ButtonMixins = {
- computed: {
- namespace: function () {
- return `${this.namespacePath}/${this.projectPath}`;
- }
- }
- };
-})(window);
diff --git a/app/assets/javascripts/diff_notes/services/resolve.js.es6 b/app/assets/javascripts/diff_notes/services/resolve.js.es6
index de771ff814b..2a55f739b31 100644
--- a/app/assets/javascripts/diff_notes/services/resolve.js.es6
+++ b/app/assets/javascripts/diff_notes/services/resolve.js.es6
@@ -9,32 +9,32 @@
Vue.http.headers.common['X-CSRF-Token'] = $.rails.csrfToken();
}
- prepareRequest(namespace) {
+ prepareRequest(root) {
this.setCSRF();
- Vue.http.options.root = `/${namespace}`;
+ Vue.http.options.root = root;
}
- resolve(namespace, noteId) {
- this.prepareRequest(namespace);
+ resolve(projectPath, noteId) {
+ this.prepareRequest(projectPath);
return this.noteResource.save({ noteId }, {});
}
- unresolve(namespace, noteId) {
- this.prepareRequest(namespace);
+ unresolve(projectPath, noteId) {
+ this.prepareRequest(projectPath);
return this.noteResource.delete({ noteId }, {});
}
- toggleResolveForDiscussion(namespace, mergeRequestId, discussionId) {
+ toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId],
isResolved = discussion.isResolved();
let promise;
if (isResolved) {
- promise = this.unResolveAll(namespace, mergeRequestId, discussionId);
+ promise = this.unResolveAll(projectPath, mergeRequestId, discussionId);
} else {
- promise = this.resolveAll(namespace, mergeRequestId, discussionId);
+ promise = this.resolveAll(projectPath, mergeRequestId, discussionId);
}
promise.then((response) => {
@@ -57,10 +57,10 @@
})
}
- resolveAll(namespace, mergeRequestId, discussionId) {
+ resolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(namespace);
+ this.prepareRequest(projectPath);
discussion.loading = true;
@@ -70,10 +70,10 @@
}, {});
}
- unResolveAll(namespace, mergeRequestId, discussionId) {
+ unResolveAll(projectPath, mergeRequestId, discussionId) {
const discussion = CommentsStore.state[discussionId];
- this.prepareRequest(namespace);
+ this.prepareRequest(projectPath);
discussion.loading = true;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index ddf11ecf34c..adff73af79c 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -26,7 +26,7 @@
case 'projects:merge_requests:index':
case 'projects:issues:index':
Issuable.init();
- new IssuableBulkActions();
+ new gl.IssuableBulkActions();
shortcut_handler = new ShortcutsNavigation();
break;
case 'projects:issues:show':
@@ -40,7 +40,7 @@
new Milestone();
break;
case 'dashboard:todos:index':
- new Todos();
+ new gl.Todos();
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
@@ -59,7 +59,9 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.issue-form'));
new IssuableForm($('.issue-form'));
- new IssuableTemplateSelectors();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new gl.IssuableTemplateSelectors();
break;
case 'projects:merge_requests:new':
case 'projects:merge_requests:edit':
@@ -67,7 +69,9 @@
shortcut_handler = new ShortcutsNavigation();
new GLForm($('.merge-request-form'));
new IssuableForm($('.merge-request-form'));
- new IssuableTemplateSelectors();
+ new LabelsSelect();
+ new MilestoneSelect();
+ new gl.IssuableTemplateSelectors();
break;
case 'projects:tags:new':
new ZenMode();
@@ -122,6 +126,9 @@
new TreeView();
}
break;
+ case 'projects:pipelines:show':
+ new gl.Pipelines();
+ break;
case 'groups:activity':
new Activities();
break;
@@ -165,7 +172,7 @@
break;
case 'projects:labels:index':
if ($('.prioritized-labels').length) {
- new LabelManager();
+ new gl.LabelManager();
}
break;
case 'projects:network:show':
@@ -279,7 +286,7 @@
Dispatcher.prototype.initSearch = function() {
// Only when search form is present
if ($('.search').length) {
- return new SearchAutocomplete();
+ return new gl.SearchAutocomplete();
}
};
diff --git a/app/assets/javascripts/gfm_auto_complete.js.es6 b/app/assets/javascripts/gfm_auto_complete.js.es6
index d0786bf0053..845313b6b38 100644
--- a/app/assets/javascripts/gfm_auto_complete.js.es6
+++ b/app/assets/javascripts/gfm_auto_complete.js.es6
@@ -52,37 +52,27 @@
}
}
},
- setup: function(input) {
+ setup: _.debounce(function(input) {
// Add GFM auto-completion to all input fields, that accept GFM input.
this.input = input || $('.js-gfm-input');
// destroy previous instances
this.destroyAtWho();
// set up instances
this.setupAtWho();
- if (this.dataSource) {
- if (!this.dataLoading && !this.cachedData) {
- this.dataLoading = true;
- setTimeout((function(_this) {
- return function() {
- var fetch;
- fetch = _this.fetchData(_this.dataSource);
- return fetch.done(function(data) {
- _this.dataLoading = false;
- return _this.loadData(data);
- });
- };
- // We should wait until initializations are done
- // and only trigger the last .setup since
- // The previous .dataSource belongs to the previous issuable
- // and the last one will have the **proper** .dataSource property
- // TODO: Make this a singleton and turn off events when moving to another page
- })(this), 1000);
- }
- if (this.cachedData != null) {
- return this.loadData(this.cachedData);
- }
+
+ if (this.dataSource && !this.dataLoading && !this.cachedData) {
+ this.dataLoading = true;
+ return this.fetchData(this.dataSource)
+ .done((data) => {
+ this.dataLoading = false;
+ this.loadData(data);
+ });
+ };
+
+ if (this.cachedData != null) {
+ return this.loadData(this.cachedData);
}
- },
+ }, 1000),
setupAtWho: function() {
// Emoji
this.input.atwho({
diff --git a/app/assets/javascripts/gl_dropdown.js b/app/assets/javascripts/gl_dropdown.js
index 1b6db641200..e034ca68645 100644
--- a/app/assets/javascripts/gl_dropdown.js
+++ b/app/assets/javascripts/gl_dropdown.js
@@ -443,6 +443,7 @@
var contentHtml;
this.resetRows();
this.addArrowKeyEvent();
+
if (this.options.setIndeterminateIds) {
this.options.setIndeterminateIds.call(this);
}
@@ -460,9 +461,21 @@
if (this.options.filterable) {
this.filterInput.focus();
}
+
+ if (this.options.showMenuAbove) {
+ this.positionMenuAbove();
+ }
+
return this.dropdown.trigger('shown.gl.dropdown');
};
+ GitLabDropdown.prototype.positionMenuAbove = function() {
+ var $button = $(this.el);
+ var $menu = this.dropdown.find('.dropdown-menu');
+
+ $menu.css('top', ($button.height() + $menu.height()) * -1);
+ };
+
GitLabDropdown.prototype.hidden = function(e) {
var $input;
this.resetRows();
@@ -725,6 +738,7 @@
return false;
}
if (currentKeyCode === 13 && currentIndex !== -1) {
+ e.preventDefault();
_this.selectRowAtIndex();
}
};
diff --git a/app/assets/javascripts/groups_select.js b/app/assets/javascripts/groups_select.js
index 7c2eebcdd44..5f06186504b 100644
--- a/app/assets/javascripts/groups_select.js
+++ b/app/assets/javascripts/groups_select.js
@@ -5,14 +5,15 @@
function GroupsSelect() {
$('.ajax-groups-select').each((function(_this) {
return function(i, select) {
- var skip_ldap;
+ var skip_ldap, skip_groups;
skip_ldap = $(select).hasClass('skip_ldap');
+ skip_groups = $(select).data('skip-groups') || [];
return $(select).select2({
placeholder: "Search for a group",
multiple: $(select).hasClass('multiselect'),
minimumInputLength: 0,
query: function(query) {
- return Api.groups(query.term, skip_ldap, function(groups) {
+ return Api.groups(query.term, skip_ldap, skip_groups, function(groups) {
var data;
data = {
results: groups
diff --git a/app/assets/javascripts/issuable.js.es6 b/app/assets/javascripts/issuable.js.es6
index 73e2664e9c0..57f7e4ef230 100644
--- a/app/assets/javascripts/issuable.js.es6
+++ b/app/assets/javascripts/issuable.js.es6
@@ -51,7 +51,6 @@
}).remove();
// Submit the form to get new data
Issuable.filterResults($('.filter-form'));
- return $('.js-label-select').trigger('update.label');
});
},
filterResults: (function(_this) {
diff --git a/app/assets/javascripts/issues-bulk-assignment.js b/app/assets/javascripts/issues_bulk_assignment.js.es6
index 62a7fc9a06c..0808f538f01 100644
--- a/app/assets/javascripts/issues-bulk-assignment.js
+++ b/app/assets/javascripts/issues_bulk_assignment.js.es6
@@ -1,13 +1,10 @@
-(function() {
- this.IssuableBulkActions = (function() {
- function IssuableBulkActions(opts) {
- // Set defaults
- var ref, ref1, ref2;
- if (opts == null) {
- opts = {};
- }
- this.container = (ref = opts.container) != null ? ref : $('.content'), this.form = (ref1 = opts.form) != null ? ref1 : this.getElement('.bulk-update'), this.issues = (ref2 = opts.issues) != null ? ref2 : this.getElement('.issuable-list > li');
- // Save instance
+((global) => {
+
+ class IssuableBulkActions {
+ constructor({ container, form, issues } = {}) {
+ this.container = container || $('.content'),
+ this.form = form || this.getElement('.bulk-update');
+ this.issues = issues || this.getElement('.issues-list .issue');
this.form.data('bulkActions', this);
this.willUpdateLabels = false;
this.bindEvents();
@@ -15,53 +12,46 @@
Issuable.initChecks();
}
- IssuableBulkActions.prototype.getElement = function(selector) {
+ getElement(selector) {
return this.container.find(selector);
- };
+ }
- IssuableBulkActions.prototype.bindEvents = function() {
+ bindEvents() {
return this.form.off('submit').on('submit', this.onFormSubmit.bind(this));
- };
+ }
- IssuableBulkActions.prototype.onFormSubmit = function(e) {
+ onFormSubmit(e) {
e.preventDefault();
return this.submit();
- };
+ }
- IssuableBulkActions.prototype.submit = function() {
- var _this, xhr;
- _this = this;
- xhr = $.ajax({
+ submit() {
+ const _this = this;
+ const xhr = $.ajax({
url: this.form.attr('action'),
method: this.form.attr('method'),
dataType: 'JSON',
data: this.getFormDataAsObject()
});
- xhr.done(function(response, status, xhr) {
- return location.reload();
- });
- xhr.fail(function() {
- return new Flash("Issue update failed");
- });
+ xhr.done(() => window.location.reload());
+ xhr.fail(() => new Flash("Issue update failed"));
return xhr.always(this.onFormSubmitAlways.bind(this));
- };
+ }
- IssuableBulkActions.prototype.onFormSubmitAlways = function() {
+ onFormSubmitAlways() {
return this.form.find('[type="submit"]').enable();
- };
+ }
- IssuableBulkActions.prototype.getSelectedIssues = function() {
+ getSelectedIssues() {
return this.issues.has('.selected_issue:checked');
- };
+ }
- IssuableBulkActions.prototype.getLabelsFromSelection = function() {
- var labels;
- labels = [];
+ getLabelsFromSelection() {
+ const labels = [];
this.getSelectedIssues().map(function() {
- var _labels;
- _labels = $(this).data('labels');
- if (_labels) {
- return _labels.map(function(labelId) {
+ const labelsData = $(this).data('labels');
+ if (labelsData) {
+ return labelsData.map(function(labelId) {
if (labels.indexOf(labelId) === -1) {
return labels.push(labelId);
}
@@ -69,7 +59,7 @@
}
});
return labels;
- };
+ }
/**
@@ -77,25 +67,21 @@
* @return {Array} Label IDs
*/
- IssuableBulkActions.prototype.getUnmarkedIndeterminedLabels = function() {
- var el, i, id, j, labelsToKeep, len, len1, ref, ref1, result;
- result = [];
- labelsToKeep = [];
- ref = this.getElement('.labels-filter .is-indeterminate');
- for (i = 0, len = ref.length; i < len; i++) {
- el = ref[i];
- labelsToKeep.push($(el).data('labelId'));
- }
- ref1 = this.getLabelsFromSelection();
- for (j = 0, len1 = ref1.length; j < len1; j++) {
- id = ref1[j];
- // Only the ones that we are not going to keep
+ getUnmarkedIndeterminedLabels() {
+ const result = [];
+ const labelsToKeep = [];
+
+ this.getElement('.labels-filter .is-indeterminate')
+ .each((i, el) => labelsToKeep.push($(el).data('labelId')));
+
+ this.getLabelsFromSelection().forEach((id) => {
if (labelsToKeep.indexOf(id) === -1) {
result.push(id);
}
- }
+ });
+
return result;
- };
+ }
/**
@@ -103,9 +89,8 @@
* Returns key/value pairs from form data
*/
- IssuableBulkActions.prototype.getFormDataAsObject = function() {
- var formData;
- formData = {
+ getFormDataAsObject() {
+ const formData = {
update: {
state_event: this.form.find('input[name="update[state_event]"]').val(),
assignee_id: this.form.find('input[name="update[assignee_id]"]').val(),
@@ -125,19 +110,18 @@
});
}
return formData;
- };
+ }
- IssuableBulkActions.prototype.getLabelsToApply = function() {
- var $labels, labelIds;
- labelIds = [];
- $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
+ getLabelsToApply() {
+ const labelIds = [];
+ const $labels = this.form.find('.labels-filter input[name="update[label_ids][]"]');
$labels.each(function(k, label) {
if (label) {
return labelIds.push(parseInt($(label).val()));
}
});
return labelIds;
- };
+ }
/**
@@ -145,11 +129,10 @@
* @return {Array} Array of labels IDs
*/
- IssuableBulkActions.prototype.getLabelsToRemove = function() {
- var indeterminatedLabels, labelsToApply, result;
- result = [];
- indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
- labelsToApply = this.getLabelsToApply();
+ getLabelsToRemove() {
+ const result = [];
+ const indeterminatedLabels = this.getUnmarkedIndeterminedLabels();
+ const labelsToApply = this.getLabelsToApply();
indeterminatedLabels.map(function(id) {
// We need to exclude label IDs that will be applied
// By not doing this will cause issues from selection to not add labels at all
@@ -158,10 +141,9 @@
}
});
return result;
- };
-
- return IssuableBulkActions;
+ }
+ }
- })();
+ global.IssuableBulkActions = IssuableBulkActions;
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/label_manager.js.es6 b/app/assets/javascripts/label_manager.js.es6
new file mode 100644
index 00000000000..bc68e53504f
--- /dev/null
+++ b/app/assets/javascripts/label_manager.js.es6
@@ -0,0 +1,106 @@
+((global) => {
+
+ class LabelManager {
+ constructor({ togglePriorityButton, prioritizedLabels, otherLabels } = {}) {
+ this.togglePriorityButton = togglePriorityButton || $('.js-toggle-priority');
+ this.prioritizedLabels = prioritizedLabels || $('.js-prioritized-labels');
+ this.otherLabels = otherLabels || $('.js-other-labels');
+ this.errorMessage = 'Unable to update label prioritization at this time';
+ this.prioritizedLabels.sortable({
+ items: 'li',
+ placeholder: 'list-placeholder',
+ axis: 'y',
+ update: this.onPrioritySortUpdate.bind(this)
+ });
+ this.bindEvents();
+ }
+
+ bindEvents() {
+ return this.togglePriorityButton.on('click', this, this.onTogglePriorityClick);
+ }
+
+ onTogglePriorityClick(e) {
+ e.preventDefault();
+ const _this = e.data;
+ const $btn = $(e.currentTarget);
+ const $label = $(`#${$btn.data('domId')}`);
+ const action = $btn.parents('.js-prioritized-labels').length ? 'remove' : 'add';
+ const $tooltip = $(`#${$btn.find('.has-tooltip:visible').attr('aria-describedby')}`);
+ $tooltip.tooltip('destroy');
+ return _this.toggleLabelPriority($label, action);
+ }
+
+ toggleLabelPriority($label, action, persistState) {
+ if (persistState == null) {
+ persistState = true;
+ }
+ let xhr;
+ const _this = this;
+ const url = $label.find('.js-toggle-priority').data('url');
+ let $target = this.prioritizedLabels;
+ let $from = this.otherLabels;
+ if (action === 'remove') {
+ $target = this.otherLabels;
+ $from = this.prioritizedLabels;
+ }
+ if ($from.find('li').length === 1) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ if (!$target.find('li').length) {
+ $target.find('.empty-message').addClass('hidden');
+ }
+ $label.detach().appendTo($target);
+ // Return if we are not persisting state
+ if (!persistState) {
+ return;
+ }
+ if (action === 'remove') {
+ xhr = $.ajax({
+ url,
+ type: 'DELETE'
+ });
+ // Restore empty message
+ if (!$from.find('li').length) {
+ $from.find('.empty-message').removeClass('hidden');
+ }
+ } else {
+ xhr = this.savePrioritySort($label, action);
+ }
+ return xhr.fail(this.rollbackLabelPosition.bind(this, $label, action));
+ }
+
+ onPrioritySortUpdate() {
+ const xhr = this.savePrioritySort();
+ return xhr.fail(function() {
+ return new Flash(this.errorMessage, 'alert');
+ });
+ }
+
+ savePrioritySort() {
+ return $.post({
+ url: this.prioritizedLabels.data('url'),
+ data: {
+ label_ids: this.getSortedLabelsIds()
+ }
+ });
+ }
+
+ rollbackLabelPosition($label, originalAction) {
+ const action = originalAction === 'remove' ? 'add' : 'remove';
+ this.toggleLabelPriority($label, action, false);
+ return new Flash(this.errorMessage, 'alert');
+ }
+
+ getSortedLabelsIds() {
+ const sortedIds = [];
+ this.prioritizedLabels.find('li').each(function() {
+ sortedIds.push($(this).data('id'));
+ });
+ return sortedIds;
+ }
+ }
+
+ gl.LabelManager = LabelManager;
+
+})(window.gl || (window.gl = {}));
+
diff --git a/app/assets/javascripts/labels_select.js b/app/assets/javascripts/labels_select.js
index 3f15a117ca8..e356872624a 100644
--- a/app/assets/javascripts/labels_select.js
+++ b/app/assets/javascripts/labels_select.js
@@ -4,9 +4,11 @@
var _this;
_this = this;
$('.js-label-select').each(function(i, dropdown) {
- var $block, $colorPreview, $dropdown, $form, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, defaultLabel, enableLabelCreateButton, issueURLSplit, issueUpdateURL, labelHTMLTemplate, labelNoneHTMLTemplate, labelUrl, projectId, saveLabelData, selectedLabel, showAny, showNo, $sidebarLabelTooltip;
+ 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;
$dropdown = $(dropdown);
- projectId = $dropdown.data('project-id');
+ $toggleText = $dropdown.find('.dropdown-toggle-text');
+ namespacePath = $dropdown.data('namespace-path');
+ projectPath = $dropdown.data('project-path');
labelUrl = $dropdown.data('labels');
issueUpdateURL = $dropdown.data('issueUpdate');
selectedLabel = $dropdown.data('selected');
@@ -15,6 +17,7 @@
}
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
defaultLabel = $dropdown.data('default-label');
abilityName = $dropdown.data('ability-name');
$selectbox = $dropdown.closest('.selectbox');
@@ -24,6 +27,14 @@
$sidebarLabelTooltip = $block.find('.js-sidebar-labels-tooltip');
$value = $block.find('.value');
$loading = $block.find('.block-loading').fadeOut();
+ fieldName = $dropdown.data('field-name');
+ useId = $dropdown.is('.js-issuable-form-dropdown, .js-filter-bulk-update, .js-label-sidebar-dropdown');
+ propertyName = useId ? 'id' : 'title';
+ initialSelected = $selectbox
+ .find('input[name="' + $dropdown.data('field-name') + '"]')
+ .map(function () {
+ return this.value;
+ }).get();
if (issueUpdateURL != null) {
issueURLSplit = issueUpdateURL.split('/');
}
@@ -35,14 +46,18 @@
$sidebarLabelTooltip.tooltip();
if ($dropdown.closest('.dropdown').find('.dropdown-new-label').length) {
- new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), projectId);
+ new gl.CreateLabelDropdown($dropdown.closest('.dropdown').find('.dropdown-new-label'), namespacePath, projectPath);
}
saveLabelData = function() {
var data, selected;
- selected = $dropdown.closest('.selectbox').find("input[name='" + ($dropdown.data('field-name')) + "']").map(function() {
+ selected = $dropdown.closest('.selectbox').find("input[name='" + fieldName + "']").map(function() {
return this.value;
}).get();
+
+ if (_.isEqual(initialSelected, selected)) return;
+ initialSelected = selected;
+
data = {};
data[abilityName] = {};
data[abilityName].label_ids = selected;
@@ -66,7 +81,8 @@
if (data.labels.length) {
template = labelHTMLTemplate(data);
labelCount = data.labels.length;
- } else {
+ }
+ else {
template = labelNoneHTMLTemplate;
}
$value.removeAttr('style').html(template);
@@ -83,7 +99,8 @@
}
labelTooltipTitle = labelTitles.join(', ');
- } else {
+ }
+ else {
labelTooltipTitle = '';
$sidebarLabelTooltip.tooltip('destroy');
}
@@ -105,6 +122,7 @@
});
};
return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: labelUrl
@@ -124,23 +142,29 @@
};
}).value();
if ($dropdown.hasClass('js-extra-options')) {
+ var extraData = [];
if (showNo) {
- data.unshift({
+ extraData.unshift({
id: 0,
title: 'No Label'
});
}
if (showAny) {
- data.unshift({
+ extraData.unshift({
isAny: true,
title: 'Any Label'
});
}
- if (data.length > 2) {
- data.splice(2, 0, 'divider');
+ if (extraData.length) {
+ extraData.push('divider');
+ data = extraData.concat(data);
}
}
- return callback(data);
+
+ callback(data);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
});
},
renderRow: function(label, instance) {
@@ -148,7 +172,7 @@
$li = $('<li>');
$a = $('<a href="#">');
selectedClass = [];
- removesAll = label.id === 0 || (label.id == null);
+ removesAll = label.id <= 0 || (label.id == null);
if ($dropdown.hasClass('js-filter-bulk-update')) {
indeterminate = instance.indeterminateIds;
active = instance.activeIds;
@@ -185,14 +209,16 @@
return color + " " + percentFirst + "%," + color + " " + percentSecond + "% ";
}).join(',');
color = "linear-gradient(" + color + ")";
- } else {
+ }
+ else {
if (label.color != null) {
color = label.color[0];
}
}
if (color) {
colorEl = "<span class='dropdown-label-box' style='background: " + color + "'></span>";
- } else {
+ }
+ else {
colorEl = '';
}
// We need to identify which items are actually labels
@@ -210,30 +236,46 @@
},
selectable: true,
filterable: true,
+ selected: $dropdown.data('selected') || [],
toggleLabel: function(selected, el) {
- var selected_labels;
- selected_labels = $('.js-label-select').siblings('.dropdown-menu-labels').find('.is-active');
- if (selected && (selected.title != null)) {
- if (selected_labels.length > 1) {
- return selected.title + " +" + (selected_labels.length - 1) + " more";
- } else {
- return selected.title;
- }
- } else if (!selected && selected_labels.length !== 0) {
- if (selected_labels.length > 1) {
- return ($(selected_labels[0]).text()) + " +" + (selected_labels.length - 1) + " more";
- } else if (selected_labels.length === 1) {
- return $(selected_labels).text();
- }
- } else {
+ var isSelected = el !== null ? el.hasClass('is-active') : false;
+ var title = selected.title;
+ var selectedLabels = this.selected;
+
+ if (selected.id === 0) {
+ this.selected = [];
+ return 'No Label';
+ }
+ else if (isSelected) {
+ this.selected.push(title);
+ }
+ else {
+ var index = this.selected.indexOf(title);
+ this.selected.splice(index, 1);
+ }
+
+ if (selectedLabels.length === 1) {
+ return selectedLabels;
+ }
+ else if (selectedLabels.length) {
+ return selectedLabels[0] + " +" + (selectedLabels.length - 1) + " more";
+ }
+ else {
return defaultLabel;
}
},
fieldName: $dropdown.data('field-name'),
id: function(label) {
+ if (label.id <= 0) return;
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return label.id;
+ }
+
if ($dropdown.hasClass("js-filter-submit") && (label.isAny == null)) {
return label.title;
- } else {
+ }
+ else {
return label.id;
}
},
@@ -245,6 +287,11 @@
$selectbox.hide();
// display:block overrides the hide-collapse rule
$value.removeAttr('style');
+
+ if ($dropdown.hasClass('js-issuable-form-dropdown')) {
+ return;
+ }
+
if (page === 'projects:boards:show') {
return;
}
@@ -252,9 +299,11 @@
if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
selectedLabels = $dropdown.closest('form').find("input:hidden[name='" + ($dropdown.data('fieldName')) + "']");
Issuable.filterResults($dropdown.closest('form'));
- } else if ($dropdown.hasClass('js-filter-submit')) {
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
$dropdown.closest('form').submit();
- } else {
+ }
+ else {
if (!$dropdown.hasClass('js-filter-bulk-update')) {
saveLabelData();
}
@@ -271,21 +320,31 @@
clicked: function(label, $el, e) {
var isIssueIndex, isMRIndex, page;
_this.enableBulkLabelDropdown();
- if ($dropdown.hasClass('js-filter-bulk-update')) {
+
+ if ($dropdown.parent().find('.is-active:not(.dropdown-clear-active)').length) {
+ $dropdown.parent()
+ .find('.dropdown-clear-active')
+ .removeClass('is-active')
+ }
+
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
return;
}
+
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = page === 'projects:merge_requests:index';
if (page === 'projects:boards:show') {
if (label.isAny) {
gl.issueBoards.BoardsStore.state.filters['label_name'] = [];
- } else if (label.title) {
+ }
+ else if ($el.hasClass('is-active')) {
gl.issueBoards.BoardsStore.state.filters['label_name'].push(label.title);
- } else {
+ }
+ else {
var filters = gl.issueBoards.BoardsStore.state.filters['label_name'];
- filters = filters.filter(function (label) {
- return label !== $el.text().trim();
+ filters = filters.filter(function (filteredLabel) {
+ return filteredLabel !== label.title;
});
gl.issueBoards.BoardsStore.state.filters['label_name'] = filters;
}
@@ -293,17 +352,21 @@
gl.issueBoards.BoardsStore.updateFiltersUrl();
e.preventDefault();
return;
- } else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
+ }
+ else if ($dropdown.hasClass('js-filter-submit') && (isIssueIndex || isMRIndex)) {
if (!$dropdown.hasClass('js-multiselect')) {
selectedLabel = label.title;
return Issuable.filterResults($dropdown.closest('form'));
}
- } else if ($dropdown.hasClass('js-filter-submit')) {
+ }
+ else if ($dropdown.hasClass('js-filter-submit')) {
return $dropdown.closest('form').submit();
- } else {
+ }
+ else {
if ($dropdown.hasClass('js-multiselect')) {
- } else {
+ }
+ else {
return saveLabelData();
}
}
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 9299d0eabd2..b170e26eebf 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -38,6 +38,11 @@
gl.utils.getPagePath = function() {
return $('body').data('page').split(':')[0];
};
+ gl.utils.parseUrl = function (url) {
+ var parser = document.createElement('a');
+ parser.href = url;
+ return parser;
+ };
return jQuery.timefor = function(time, suffix, expiredLabel) {
var suffixFromNow, timefor;
if (!time) {
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index f84a20cf0fe..b8d52becb3f 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -19,7 +19,7 @@
while (i < sURLVariables.length) {
sParameterName = sURLVariables[i].split('=');
if (sParameterName[0] === sParam) {
- values.push(sParameterName[1]);
+ values.push(sParameterName[1].replace(/\+/g, ' '));
}
i++;
}
diff --git a/app/assets/javascripts/merge_conflict_data_provider.js.es6 b/app/assets/javascripts/merge_conflict_data_provider.js.es6
index cd92df8ddc5..13ee794ba38 100644
--- a/app/assets/javascripts/merge_conflict_data_provider.js.es6
+++ b/app/assets/javascripts/merge_conflict_data_provider.js.es6
@@ -7,13 +7,16 @@ const ORIGIN_BUTTON_TITLE = 'Use theirs';
class MergeConflictDataProvider {
getInitialData() {
+ // TODO: remove reliance on jQuery and DOM state introspection
const diffViewType = $.cookie('diff_view');
+ const fixedLayout = $('.content-wrapper .container-fluid').hasClass('container-limited');
return {
isLoading : true,
hasError : false,
isParallel : diffViewType === 'parallel',
diffViewType : diffViewType,
+ fixedLayout : fixedLayout,
isSubmitting : false,
conflictsData : {},
resolutionData : {}
@@ -192,14 +195,17 @@ class MergeConflictDataProvider {
updateViewType(newType) {
const vi = this.vueInstance;
- if (newType === vi.diffView || !(newType === 'parallel' || newType === 'inline')) {
+ if (newType === vi.diffViewType || !(newType === 'parallel' || newType === 'inline')) {
return;
}
- vi.diffView = newType;
- vi.isParallel = newType === 'parallel';
- $.cookie('diff_view', newType); // TODO: Make sure that cookie path added.
- $('.content-wrapper .container-fluid').toggleClass('container-limited');
+ vi.diffViewType = newType;
+ vi.isParallel = newType === 'parallel';
+ $.cookie('diff_view', newType, {
+ path: (gon && gon.relative_url_root) || '/'
+ });
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', !vi.isParallel && vi.fixedLayout);
}
diff --git a/app/assets/javascripts/merge_conflict_resolver.js.es6 b/app/assets/javascripts/merge_conflict_resolver.js.es6
index b56fd5aa658..7e756433bf5 100644
--- a/app/assets/javascripts/merge_conflict_resolver.js.es6
+++ b/app/assets/javascripts/merge_conflict_resolver.js.es6
@@ -60,9 +60,8 @@ class MergeConflictResolver {
$('#conflicts .js-syntax-highlight').syntaxHighlight();
});
- if (this.vue.diffViewType === 'parallel') {
- $('.content-wrapper .container-fluid').removeClass('container-limited');
- }
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', !this.vue.isParallel && this.vue.fixedLayout);
})
}
diff --git a/app/assets/javascripts/merge_request.js b/app/assets/javascripts/merge_request.js
index 05644b3d03c..02ff5a382e2 100644
--- a/app/assets/javascripts/merge_request.js
+++ b/app/assets/javascripts/merge_request.js
@@ -36,13 +36,10 @@
};
MergeRequest.prototype.initTabs = function() {
- if (this.opts.action !== 'new') {
- // `MergeRequests#new` has no tab-persisting or lazy-loading behavior
- window.mrTabs = new MergeRequestTabs(this.opts);
- } else {
- // Show the first tab (Commits)
- return $('.merge-request-tabs a[data-toggle="tab"]:first').tab('show');
+ if (window.mrTabs) {
+ window.mrTabs.unbindEvents();
}
+ window.mrTabs = new MergeRequestTabs(this.opts);
};
MergeRequest.prototype.showAllCommits = function() {
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 18bbfa7a459..8045d24a1bb 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -56,9 +56,14 @@
MergeRequestTabs.prototype.commitsLoaded = false;
+ MergeRequestTabs.prototype.fixedLayoutPref = null;
+
function MergeRequestTabs(opts) {
this.opts = opts != null ? opts : {};
this.opts.setUrl = this.opts.setUrl !== undefined ? this.opts.setUrl : true;
+
+ this.buildsLoaded = this.opts.buildsLoaded || false;
+
this.setCurrentAction = bind(this.setCurrentAction, this);
this.tabShown = bind(this.tabShown, this);
this.showTab = bind(this.showTab, this);
@@ -70,7 +75,12 @@
MergeRequestTabs.prototype.bindEvents = function() {
$(document).on('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
- return $(document).on('click', '.js-show-tab', this.showTab);
+ $(document).on('click', '.js-show-tab', this.showTab);
+ };
+
+ MergeRequestTabs.prototype.unbindEvents = function() {
+ $(document).off('shown.bs.tab', '.merge-request-tabs a[data-toggle="tab"]', this.tabShown);
+ $(document).off('click', '.js-show-tab', this.showTab);
};
MergeRequestTabs.prototype.showTab = function(event) {
@@ -85,11 +95,15 @@
if (action === 'commits') {
this.loadCommits($target.attr('href'));
this.expandView();
- } else if (action === 'diffs') {
+ this.resetViewContainer();
+ } else if (this.isDiffAction(action)) {
this.loadDiff($target.attr('href'));
if ((typeof bp !== "undefined" && bp !== null) && bp.getBreakpointSize() !== 'lg') {
this.shrinkView();
}
+ if (this.diffViewType() === 'parallel') {
+ this.expandViewContainer();
+ }
navBarHeight = $('.navbar-gitlab').outerHeight();
$.scrollTo(".merge-request-details .merge-request-tabs", {
offset: -navBarHeight
@@ -97,11 +111,14 @@
} else if (action === 'builds') {
this.loadBuilds($target.attr('href'));
this.expandView();
+ this.resetViewContainer();
} else if (action === 'pipelines') {
this.loadPipelines($target.attr('href'));
this.expandView();
+ this.resetViewContainer();
} else {
this.expandView();
+ this.resetViewContainer();
}
if (this.opts.setUrl) {
this.setCurrentAction(action);
@@ -126,7 +143,7 @@
if (action === 'show') {
action = 'notes';
}
- return $(".merge-request-tabs a[data-action='" + action + "']").tab('show');
+ $(".merge-request-tabs a[data-action='" + action + "']").tab('show').trigger('shown.bs.tab');
};
// Replaces the current Merge Request-specific action in the URL with a new one
@@ -156,8 +173,9 @@
action = 'notes';
}
this.currentAction = action;
- // Remove a trailing '/commits' or '/diffs'
- new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines)(\.html)?\/?$/, '');
+ // Remove a trailing '/commits' '/diffs' '/builds' '/pipelines' '/new' '/new/diffs'
+ new_state = this._location.pathname.replace(/\/(commits|diffs|builds|pipelines|new|new\/diffs)(\.html)?\/?$/, '');
+
// Append the new action if we're on a tab other than 'notes'
if (action !== 'notes') {
new_state += "/" + action;
@@ -196,8 +214,13 @@
if (this.diffsLoaded) {
return;
}
+
+ // We extract pathname for the current Changes tab anchor href
+ // some pages like MergeRequestsController#new has query parameters on that anchor
+ var url = gl.utils.parseUrl(source);
+
return this._get({
- url: (source + ".json") + this._location.search,
+ url: (url.pathname + ".json") + this._location.search,
success: (function(_this) {
return function(data) {
$('#diffs').html(data.html);
@@ -209,7 +232,7 @@
gl.utils.localTimeAgo($('.js-timeago', 'div#diffs'));
$('#diffs .js-syntax-highlight').syntaxHighlight();
$('#diffs .diff-file').singleFileDiff();
- if (_this.diffViewType() === 'parallel') {
+ if (_this.diffViewType() === 'parallel' && (_this.isDiffAction(_this.currentAction)) ) {
_this.expandViewContainer();
}
_this.diffsLoaded = true;
@@ -308,11 +331,25 @@
MergeRequestTabs.prototype.diffViewType = function() {
return $('.inline-parallel-buttons a.active').data('view-type');
- // Returns diff view type
+ };
+
+ MergeRequestTabs.prototype.isDiffAction = function(action) {
+ return action === 'diffs' || action === 'new/diffs'
};
MergeRequestTabs.prototype.expandViewContainer = function() {
- return $('.container-fluid').removeClass('container-limited');
+ var $wrapper = $('.content-wrapper .container-fluid');
+ if (this.fixedLayoutPref === null) {
+ this.fixedLayoutPref = $wrapper.hasClass('container-limited');
+ }
+ $wrapper.removeClass('container-limited');
+ };
+
+ MergeRequestTabs.prototype.resetViewContainer = function() {
+ if (this.fixedLayoutPref !== null) {
+ $('.content-wrapper .container-fluid')
+ .toggleClass('container-limited', this.fixedLayoutPref);
+ }
};
MergeRequestTabs.prototype.shrinkView = function() {
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index c8031174dd2..26cc6eb0e96 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -7,7 +7,7 @@
this.currentProject = JSON.parse(currentProject);
}
$('.js-milestone-select').each(function(i, dropdown) {
- var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId;
+ var $block, $dropdown, $loading, $selectbox, $sidebarCollapsedValue, $value, abilityName, collapsedSidebarLabelTemplate, defaultLabel, issuableId, issueUpdateURL, milestoneLinkNoneTemplate, milestoneLinkTemplate, milestonesUrl, projectId, selectedMilestone, showAny, showNo, showUpcoming, useId, showMenuAbove;
$dropdown = $(dropdown);
projectId = $dropdown.data('project-id');
milestonesUrl = $dropdown.data('milestones');
@@ -15,6 +15,7 @@
selectedMilestone = $dropdown.data('selected');
showNo = $dropdown.data('show-no');
showAny = $dropdown.data('show-any');
+ showMenuAbove = $dropdown.data('showMenuAbove');
showUpcoming = $dropdown.data('show-upcoming');
useId = $dropdown.data('use-id');
defaultLabel = $dropdown.data('default-label');
@@ -31,12 +32,12 @@
collapsedSidebarLabelTemplate = _.template('<span class="has-tooltip" data-container="body" title="<%- remaining %>" data-placement="left"> <%- title %> </span>');
}
return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
data: function(term, callback) {
return $.ajax({
url: milestonesUrl
}).done(function(data) {
- var extraOptions;
- extraOptions = [];
+ var extraOptions = [];
if (showAny) {
extraOptions.push({
id: 0,
@@ -58,10 +59,14 @@
title: 'Upcoming'
});
}
- if (extraOptions.length > 2) {
+ if (extraOptions.length) {
extraOptions.push('divider');
}
- return callback(extraOptions.concat(data));
+
+ callback(extraOptions.concat(data));
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
});
},
filterable: true,
@@ -69,19 +74,20 @@
fields: ['title']
},
selectable: true,
- toggleLabel: function(selected) {
- if (selected && 'id' in selected) {
+ toggleLabel: function(selected, el, e) {
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
return selected.title;
} else {
return defaultLabel;
}
},
+ defaultLabel: defaultLabel,
fieldName: $dropdown.data('field-name'),
text: function(milestone) {
return _.escape(milestone.title);
},
id: function(milestone) {
- if (!useId) {
+ if (!useId && !$dropdown.is('.js-issuable-form-dropdown')) {
return milestone.name;
} else {
return milestone.id;
@@ -100,7 +106,8 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
- if ($dropdown.hasClass('js-filter-bulk-update')) {
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ e.preventDefault();
return;
}
if (page === 'projects:boards:show') {
diff --git a/app/assets/javascripts/network/branch-graph.js b/app/assets/javascripts/network/branch_graph.js
index 91132af273a..91132af273a 100644
--- a/app/assets/javascripts/network/branch-graph.js
+++ b/app/assets/javascripts/network/branch_graph.js
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index c6854f703fb..866a04d3e21 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -432,14 +432,12 @@
var $form = $(xhr.target);
if ($form.attr('data-resolve-all') != null) {
- var namespacePath = $form.attr('data-namespace-path'),
- projectPath = $form.attr('data-project-path')
- discussionId = $form.attr('data-discussion-id'),
- mergeRequestId = $form.attr('data-noteable-iid'),
- namespace = namespacePath + '/' + projectPath;
+ var projectPath = $form.data('project-path')
+ discussionId = $form.data('discussion-id'),
+ mergeRequestId = $form.data('noteable-iid');
if (ResolveService != null) {
- ResolveService.toggleResolveForDiscussion(namespace, mergeRequestId, discussionId);
+ ResolveService.toggleResolveForDiscussion(projectPath, mergeRequestId, discussionId);
}
}
@@ -854,7 +852,6 @@
.closest('form')
.attr('data-discussion-id', discussionId)
.attr('data-resolve-all', 'true')
- .attr('data-namespace-path', $this.attr('data-namespace-path'))
.attr('data-project-path', $this.attr('data-project-path'));
};
diff --git a/app/assets/javascripts/pipeline.js.es6 b/app/assets/javascripts/pipeline.js.es6
index bf33eb10100..6bf63ee6979 100644
--- a/app/assets/javascripts/pipeline.js.es6
+++ b/app/assets/javascripts/pipeline.js.es6
@@ -1,15 +1,40 @@
-(function() {
- function toggleGraph() {
- const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
- const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
- const $btnText = $(this).find('.toggle-btn-text');
+((global) => {
- $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+ class Pipelines {
+ constructor() {
+ $(document).off('click', '.toggle-pipeline-btn').on('click', '.toggle-pipeline-btn', this.toggleGraph);
+ this.addMarginToBuildColumns();
+ }
- const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
+ toggleGraph() {
+ const $pipelineBtn = $(this).closest('.toggle-pipeline-btn');
+ const $pipelineGraph = $(this).closest('.row-content-block').next('.pipeline-graph');
+ const $btnText = $(this).find('.toggle-btn-text');
+ const graphCollapsed = $pipelineGraph.hasClass('graph-collapsed');
- graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ $($pipelineBtn).add($pipelineGraph).toggleClass('graph-collapsed');
+
+
+ graphCollapsed ? $btnText.text('Expand') : $btnText.text('Hide')
+ }
+
+ addMarginToBuildColumns() {
+ const $secondChildBuildNode = $('.build:nth-child(2)');
+ if ($secondChildBuildNode.length) {
+ const $firstChildBuildNode = $secondChildBuildNode.prev('.build');
+ const $multiBuildColumn = $secondChildBuildNode.closest('.stage-column');
+ const $previousColumn = $multiBuildColumn.prev('.stage-column');
+ $multiBuildColumn.addClass('left-margin');
+ $firstChildBuildNode.addClass('left-connector');
+ $previousColumn.each(function() {
+ $this = $(this);
+ if ($('.build', $this).length === 1) $this.addClass('no-margin');
+ });
+ }
+ $('.pipeline-graph').removeClass('hidden');
+ }
}
- $(document).on('click', '.toggle-pipeline-btn', toggleGraph);
-})();
+ global.Pipelines = Pipelines;
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/gl_crop.js b/app/assets/javascripts/profile/gl_crop.js.es6
index 30cd6f6e470..a1b0126e857 100644
--- a/app/assets/javascripts/profile/gl_crop.js
+++ b/app/assets/javascripts/profile/gl_crop.js.es6
@@ -1,47 +1,45 @@
-(function() {
- var GitLabCrop,
- bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
+((global) => {
- GitLabCrop = (function() {
- var FILENAMEREGEX;
+ // Matches everything but the file name
+ const FILENAMEREGEX = /^.*[\\\/]/;
- // Matches everything but the file name
- FILENAMEREGEX = /^.*[\\\/]/;
+ class GitLabCrop {
+ constructor(input, { filename, previewImage, modalCrop, pickImageEl, uploadImageBtn, modalCropImg,
+ exportWidth = 200, exportHeight = 200, cropBoxWidth = 200, cropBoxHeight = 200 } = {}) {
- function GitLabCrop(input, opts) {
- var ref, ref1, ref2, ref3, ref4;
- if (opts == null) {
- opts = {};
- }
- this.onUploadImageBtnClick = bind(this.onUploadImageBtnClick, this);
- this.onModalHide = bind(this.onModalHide, this);
- this.onModalShow = bind(this.onModalShow, this);
- this.onPickImageClick = bind(this.onPickImageClick, this);
+ this.onUploadImageBtnClick = this.onUploadImageBtnClick.bind(this);
+ this.onModalHide = this.onModalHide.bind(this);
+ this.onModalShow = this.onModalShow.bind(this);
+ this.onPickImageClick = this.onPickImageClick.bind(this);
this.fileInput = $(input);
- // We should rename to avoid spec to fail
- // Form will submit the proper input filed with a file using FormData
- this.fileInput.attr('name', (this.fileInput.attr('name')) + "-trigger").attr('id', (this.fileInput.attr('id')) + "-trigger");
- // Set defaults
- this.exportWidth = (ref = opts.exportWidth) != null ? ref : 200, this.exportHeight = (ref1 = opts.exportHeight) != null ? ref1 : 200, this.cropBoxWidth = (ref2 = opts.cropBoxWidth) != null ? ref2 : 200, this.cropBoxHeight = (ref3 = opts.cropBoxHeight) != null ? ref3 : 200, this.form = (ref4 = opts.form) != null ? ref4 : this.fileInput.parents('form'), this.filename = opts.filename, this.previewImage = opts.previewImage, this.modalCrop = opts.modalCrop, this.pickImageEl = opts.pickImageEl, this.uploadImageBtn = opts.uploadImageBtn, this.modalCropImg = opts.modalCropImg;
- // Required params
- // Ensure needed elements are jquery objects
- // If selector is provided we will convert them to a jQuery Object
- this.filename = this.getElement(this.filename);
- this.previewImage = this.getElement(this.previewImage);
- this.pickImageEl = this.getElement(this.pickImageEl);
- // Modal elements usually are outside the @form element
- this.modalCrop = _.isString(this.modalCrop) ? $(this.modalCrop) : this.modalCrop;
- this.uploadImageBtn = _.isString(this.uploadImageBtn) ? $(this.uploadImageBtn) : this.uploadImageBtn;
this.modalCropImg = _.isString(this.modalCropImg) ? $(this.modalCropImg) : this.modalCropImg;
+ this.fileInput.attr('name', `${this.fileInput.attr('name')}-trigger`).attr('id', `this.fileInput.attr('id')-trigger`);
+ this.exportWidth = exportWidth;
+ this.exportHeight = exportHeight;
+ this.cropBoxWidth = cropBoxWidth;
+ this.cropBoxHeight = cropBoxHeight;
+ this.form = this.fileInput.parents('form');
+ this.filename = filename;
+ this.previewImage = previewImage;
+ this.modalCrop = modalCrop;
+ this.pickImageEl = pickImageEl;
+ this.uploadImageBtn = uploadImageBtn;
+ this.modalCropImg = modalCropImg;
+ this.filename = this.getElement(filename);
+ this.previewImage = this.getElement(previewImage);
+ this.pickImageEl = this.getElement(pickImageEl);
+ this.modalCrop = _.isString(modalCrop) ? $(modalCrop) : modalCrop;
+ this.uploadImageBtn = _.isString(uploadImageBtn) ? $(uploadImageBtn) : uploadImageBtn;
+ this.modalCropImg = _.isString(modalCropImg) ? $(modalCropImg) : modalCropImg;
this.cropActionsBtn = this.modalCrop.find('[data-method]');
this.bindEvents();
}
- GitLabCrop.prototype.getElement = function(selector) {
+ getElement(selector) {
return $(selector, this.form);
- };
+ }
- GitLabCrop.prototype.bindEvents = function() {
+ bindEvents() {
var _this;
_this = this;
this.fileInput.on('change', function(e) {
@@ -57,13 +55,13 @@
return _this.onActionBtnClick(btn);
});
return this.croppedImageBlob = null;
- };
+ }
- GitLabCrop.prototype.onPickImageClick = function() {
+ onPickImageClick() {
return this.fileInput.trigger('click');
- };
+ }
- GitLabCrop.prototype.onModalShow = function() {
+ onModalShow() {
var _this;
_this = this;
return this.modalCropImg.cropper({
@@ -95,44 +93,44 @@
});
}
});
- };
+ }
- GitLabCrop.prototype.onModalHide = function() {
+ onModalHide() {
return this.modalCropImg.attr('src', '').cropper('destroy');
- };
+ }
- GitLabCrop.prototype.onUploadImageBtnClick = function(e) { // Remove attached image
- e.preventDefault(); // Destroy cropper instance
+ onUploadImageBtnClick(e) {
+ e.preventDefault();
this.setBlob();
this.setPreview();
this.modalCrop.modal('hide');
return this.fileInput.val('');
- };
+ }
- GitLabCrop.prototype.onActionBtnClick = function(btn) {
+ onActionBtnClick(btn) {
var data, result;
data = $(btn).data();
if (this.modalCropImg.data('cropper') && data.method) {
return result = this.modalCropImg.cropper(data.method, data.option);
}
- };
+ }
- GitLabCrop.prototype.onFileInputChange = function(e, input) {
+ onFileInputChange(e, input) {
return this.readFile(input);
- };
+ }
- GitLabCrop.prototype.readFile = function(input) {
+ readFile(input) {
var _this, reader;
_this = this;
reader = new FileReader;
- reader.onload = function() {
+ reader.onload = () => {
_this.modalCropImg.attr('src', reader.result);
return _this.modalCrop.modal('show');
};
return reader.readAsDataURL(input.files[0]);
- };
+ }
- GitLabCrop.prototype.dataURLtoBlob = function(dataURL) {
+ dataURLtoBlob(dataURL) {
var array, binary, i, k, len, v;
binary = atob(dataURL.split(',')[1]);
array = [];
@@ -143,35 +141,32 @@
return new Blob([new Uint8Array(array)], {
type: 'image/png'
});
- };
+ }
- GitLabCrop.prototype.setPreview = function() {
+ setPreview() {
var filename;
this.previewImage.attr('src', this.dataURL);
filename = this.fileInput.val().replace(FILENAMEREGEX, '');
return this.filename.text(filename);
- };
+ }
- GitLabCrop.prototype.setBlob = function() {
+ setBlob() {
this.dataURL = this.modalCropImg.cropper('getCroppedCanvas', {
width: 200,
height: 200
}).toDataURL('image/png');
return this.croppedImageBlob = this.dataURLtoBlob(this.dataURL);
- };
+ }
- GitLabCrop.prototype.getBlob = function() {
+ getBlob() {
return this.croppedImageBlob;
- };
-
- return GitLabCrop;
-
- })();
+ }
+ }
$.fn.glCrop = function(opts) {
return this.each(function() {
return $(this).data('glcrop', new GitLabCrop(this, opts));
});
- };
+ }
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
deleted file mode 100644
index 60f9fba5777..00000000000
--- a/app/assets/javascripts/profile/profile.js
+++ /dev/null
@@ -1,106 +0,0 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Profile = (function() {
- function Profile(opts) {
- var cropOpts, ref;
- if (opts == null) {
- opts = {};
- }
- this.onSubmitForm = bind(this.onSubmitForm, this);
- this.form = (ref = opts.form) != null ? ref : $('.edit-user');
- $('.js-preferences-form').on('change.preference', 'input[type=radio]', function() {
- return $(this).parents('form').submit();
- // Automatically submit the Preferences form when any of its radio buttons change
- });
- $('#user_notification_email').on('change', function() {
- return $(this).parents('form').submit();
- // Automatically submit email form when it changes
- });
- $('.update-username').on('ajax:before', function() {
- $('.loading-username').show();
- $(this).find('.update-success').hide();
- return $(this).find('.update-failed').hide();
- });
- $('.update-username').on('ajax:complete', function() {
- $('.loading-username').hide();
- $(this).find('.btn-save').enable();
- return $(this).find('.loading-gif').hide();
- });
- $('.update-notifications').on('ajax:success', function(e, data) {
- if (data.saved) {
- return new Flash("Notification settings saved", "notice");
- } else {
- return new Flash("Failed to save new settings", "alert");
- }
- });
- this.bindEvents();
- 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');
- }
-
- Profile.prototype.bindEvents = function() {
- return this.form.on('submit', this.onSubmitForm);
- };
-
- Profile.prototype.onSubmitForm = function(e) {
- e.preventDefault();
- return this.saveForm();
- };
-
- Profile.prototype.saveForm = function() {
- var avatarBlob, formData, self;
- self = this;
- formData = new FormData(this.form[0]);
- avatarBlob = this.avatarGlCrop.getBlob();
- if (avatarBlob != null) {
- formData.append('user[avatar]', avatarBlob, 'avatar.png');
- }
- return $.ajax({
- url: this.form.attr('action'),
- type: this.form.attr('method'),
- data: formData,
- dataType: "json",
- processData: false,
- contentType: false,
- success: function(response) {
- return new Flash(response.message, 'notice');
- },
- error: function(jqXHR) {
- return new Flash(jqXHR.responseJSON.message, 'alert');
- },
- complete: function() {
- window.scrollTo(0, 0);
- // Enable submit button after requests ends
- return self.form.find(':input[disabled]').enable();
- }
- });
- };
-
- return Profile;
-
- })();
-
- $(function() {
- $(document).on('focusout.ssh_key', '#key_key', function() {
- var $title, comment;
- $title = $('#key_title');
- comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
- if (comment && comment.length > 1 && $title.val() === '') {
- return $title.val(comment[1]).change();
- }
- // Extract the SSH Key title from its comment
- });
- if (gl.utils.getPagePath() === 'profiles') {
- return new Profile();
- }
- });
-
-}).call(this);
diff --git a/app/assets/javascripts/profile/profile.js.es6 b/app/assets/javascripts/profile/profile.js.es6
new file mode 100644
index 00000000000..b2307be73ad
--- /dev/null
+++ b/app/assets/javascripts/profile/profile.js.es6
@@ -0,0 +1,100 @@
+((global) => {
+
+ class Profile {
+ constructor({ form } = {}) {
+ this.onSubmitForm = this.onSubmitForm.bind(this);
+ this.form = form || $('.edit-user');
+ 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');
+ }
+
+ bindEvents() {
+ $('.js-preferences-form').on('change.preference', 'input[type=radio]', this.submitForm);
+ $('#user_notification_email').on('change', this.submitForm);
+ $('.update-username').on('ajax:before', this.beforeUpdateUsername);
+ $('.update-username').on('ajax:complete', this.afterUpdateUsername);
+ $('.update-notifications').on('ajax:success', this.onUpdateNotifs);
+ this.form.on('submit', this.onSubmitForm);
+ }
+
+ submitForm() {
+ return $(this).parents('form').submit();
+ }
+
+ onSubmitForm(e) {
+ e.preventDefault();
+ return this.saveForm();
+ }
+
+ beforeUpdateUsername() {
+ $('.loading-username').show();
+ $(this).find('.update-success').hide();
+ return $(this).find('.update-failed').hide();
+ }
+
+ afterUpdateUsername() {
+ $('.loading-username').hide();
+ $(this).find('.btn-save').enable();
+ return $(this).find('.loading-gif').hide();
+ }
+
+ onUpdateNotifs(e, data) {
+ return data.saved ?
+ new Flash("Notification settings saved", "notice") :
+ new Flash("Failed to save new settings", "alert");
+ }
+
+ saveForm() {
+ const self = this;
+ const formData = new FormData(this.form[0]);
+ const avatarBlob = this.avatarGlCrop.getBlob();
+
+ if (avatarBlob != null) {
+ formData.append('user[avatar]', avatarBlob, 'avatar.png');
+ }
+
+ return $.ajax({
+ url: this.form.attr('action'),
+ type: this.form.attr('method'),
+ data: formData,
+ dataType: "json",
+ processData: false,
+ contentType: false,
+ success: response => new Flash(response.message, 'notice'),
+ error: jqXHR => new Flash(jqXHR.responseJSON.message, 'alert'),
+ complete: () => {
+ window.scrollTo(0, 0);
+ // Enable submit button after requests ends
+ return self.form.find(':input[disabled]').enable();
+ }
+ });
+ }
+ }
+
+ $(function() {
+ $(document).on('focusout.ssh_key', '#key_key', function() {
+ const $title = $('#key_title');
+ const comment = $(this).val().match(/^\S+ \S+ (.+)\n?$/);
+ if (comment && comment.length > 1 && $title.val() === '') {
+ return $title.val(comment[1]).change();
+ }
+ // Extract the SSH Key title from its comment
+ });
+ if (global.utils.getPagePath() === 'profiles') {
+ return new Profile();
+ }
+ });
+
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/project_select.js b/app/assets/javascripts/project_select.js
index 20b147500cf..4239ed2f889 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -23,7 +23,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
- return Api.groups(term, false, groupsCallback);
+ return Api.groups(term, false, false, groupsCallback);
};
} else {
projectsCallback = finalCallback;
@@ -72,7 +72,7 @@
data = groups.concat(projects);
return finalCallback(data);
};
- return Api.groups(query.term, false, groupsCallback);
+ return Api.groups(query.term, false, false, groupsCallback);
};
} else {
projectsCallback = finalCallback;
diff --git a/app/assets/javascripts/search.js b/app/assets/javascripts/search.js
index d34346f862b..8074a94f33e 100644
--- a/app/assets/javascripts/search.js
+++ b/app/assets/javascripts/search.js
@@ -10,7 +10,7 @@
filterable: true,
fieldName: 'group_id',
data: function(term, callback) {
- return Api.groups(term, null, function(data) {
+ return Api.groups(term, false, false, function(data) {
data.unshift({
name: 'Any'
});
diff --git a/app/assets/javascripts/search_autocomplete.js b/app/assets/javascripts/search_autocomplete.js.es6
index 8abb09c626f..b4c6226dc68 100644
--- a/app/assets/javascripts/search_autocomplete.js
+++ b/app/assets/javascripts/search_autocomplete.js.es6
@@ -1,30 +1,21 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.SearchAutocomplete = (function() {
- var KEYCODE;
-
- KEYCODE = {
- ESCAPE: 27,
- BACKSPACE: 8,
- ENTER: 13,
- UP: 38,
- DOWN: 40
- };
+((global) => {
- function SearchAutocomplete(opts) {
- var ref, ref1, ref2, ref3, ref4;
- if (opts == null) {
- opts = {};
- }
- this.onSearchInputBlur = bind(this.onSearchInputBlur, this);
- this.onClearInputClick = bind(this.onClearInputClick, this);
- this.onSearchInputFocus = bind(this.onSearchInputFocus, this);
- this.onSearchInputClick = bind(this.onSearchInputClick, this);
- this.onSearchInputKeyUp = bind(this.onSearchInputKeyUp, this);
- this.onSearchInputKeyDown = bind(this.onSearchInputKeyDown, this);
- this.wrap = (ref = opts.wrap) != null ? ref : $('.search'), this.optsEl = (ref1 = opts.optsEl) != null ? ref1 : this.wrap.find('.search-autocomplete-opts'), this.autocompletePath = (ref2 = opts.autocompletePath) != null ? ref2 : this.optsEl.data('autocomplete-path'), this.projectId = (ref3 = opts.projectId) != null ? ref3 : this.optsEl.data('autocomplete-project-id') || '', this.projectRef = (ref4 = opts.projectRef) != null ? ref4 : this.optsEl.data('autocomplete-project-ref') || '';
- // Dropdown Element
+ const KEYCODE = {
+ ESCAPE: 27,
+ BACKSPACE: 8,
+ ENTER: 13,
+ UP: 38,
+ DOWN: 40
+ };
+
+ class SearchAutocomplete {
+ constructor({ wrap, optsEl, autocompletePath, projectId, projectRef } = {}) {
+ this.bindEventContext();
+ this.wrap = wrap || $('.search');
+ this.optsEl = optsEl || this.wrap.find('.search-autocomplete-opts');
+ this.autocompletePath = autocompletePath || this.optsEl.data('autocomplete-path');
+ this.projectId = projectId || (this.optsEl.data('autocomplete-project-id') || '');
+ this.projectRef = projectRef || (this.optsEl.data('autocomplete-project-ref') || '');
this.dropdown = this.wrap.find('.dropdown');
this.dropdownContent = this.dropdown.find('.dropdown-content');
this.locationBadgeEl = this.getElement('.location-badge');
@@ -46,19 +37,27 @@
}
// Finds an element inside wrapper element
- SearchAutocomplete.prototype.getElement = function(selector) {
+ bindEventContext() {
+ this.onSearchInputBlur = this.onSearchInputBlur.bind(this);
+ this.onClearInputClick = this.onClearInputClick.bind(this);
+ this.onSearchInputFocus = this.onSearchInputFocus.bind(this);
+ this.onSearchInputClick = this.onSearchInputClick.bind(this);
+ this.onSearchInputKeyUp = this.onSearchInputKeyUp.bind(this);
+ this.onSearchInputKeyDown = this.onSearchInputKeyDown.bind(this);
+ }
+ getElement(selector) {
return this.wrap.find(selector);
- };
+ }
- SearchAutocomplete.prototype.saveOriginalState = function() {
+ saveOriginalState() {
return this.originalState = this.serializeState();
- };
+ }
- SearchAutocomplete.prototype.saveTextLength = function() {
+ saveTextLength() {
return this.lastTextLength = this.searchInput.val().length;
- };
+ }
- SearchAutocomplete.prototype.createAutocomplete = function() {
+ createAutocomplete() {
return this.searchInput.glDropdown({
filterInputBlur: false,
filterable: true,
@@ -73,9 +72,9 @@
selectable: true,
clicked: this.onClick.bind(this)
});
- };
+ }
- SearchAutocomplete.prototype.getData = function(term, callback) {
+ getData(term, callback) {
var _this, contents, jqXHR;
_this = this;
if (!term) {
@@ -138,9 +137,9 @@
}).always(function() {
return _this.loadingSuggestions = false;
});
- };
+ }
- SearchAutocomplete.prototype.getCategoryContents = function() {
+ getCategoryContents() {
var dashboardOptions, groupOptions, issuesPath, items, mrPath, name, options, projectOptions, userId, utils;
userId = gon.current_user_id;
utils = gl.utils, projectOptions = gl.projectOptions, groupOptions = gl.groupOptions, dashboardOptions = gl.dashboardOptions;
@@ -173,9 +172,9 @@
items.splice(0, 1);
}
return items;
- };
+ }
- SearchAutocomplete.prototype.serializeState = function() {
+ serializeState() {
return {
// Search Criteria
search_project_id: this.projectInputEl.val(),
@@ -186,9 +185,9 @@
// Location badge
_location: this.locationBadgeEl.text()
};
- };
+ }
- SearchAutocomplete.prototype.bindEvents = function() {
+ bindEvents() {
this.searchInput.on('keydown', this.onSearchInputKeyDown);
this.searchInput.on('keyup', this.onSearchInputKeyUp);
this.searchInput.on('click', this.onSearchInputClick);
@@ -200,9 +199,9 @@
return _this.searchInput.focus();
};
})(this));
- };
+ }
- SearchAutocomplete.prototype.enableAutocomplete = function() {
+ enableAutocomplete() {
var _this;
// No need to enable anything if user is not logged in
if (!gon.current_user_id) {
@@ -216,12 +215,12 @@
}
};
- SearchAutocomplete.prototype.onSearchInputKeyDown = function() {
// Saves last length of the entered text
+ onSearchInputKeyDown() {
return this.saveTextLength();
- };
+ }
- SearchAutocomplete.prototype.onSearchInputKeyUp = function(e) {
+ onSearchInputKeyUp(e) {
switch (e.keyCode) {
case KEYCODE.BACKSPACE:
// when trying to remove the location badge
@@ -259,54 +258,53 @@
}
}
this.wrap.toggleClass('has-value', !!e.target.value);
- };
+ }
// Avoid falsy value to be returned
- SearchAutocomplete.prototype.onSearchInputClick = function(e) {
- // Prevents closing the dropdown menu
+ onSearchInputClick(e) {
return e.stopImmediatePropagation();
- };
+ }
- SearchAutocomplete.prototype.onSearchInputFocus = function() {
+ onSearchInputFocus() {
this.isFocused = true;
this.wrap.addClass('search-active');
if (this.getValue() === '') {
return this.getData();
}
- };
+ }
- SearchAutocomplete.prototype.getValue = function() {
+ getValue() {
return this.searchInput.val();
- };
+ }
- SearchAutocomplete.prototype.onClearInputClick = function(e) {
+ onClearInputClick(e) {
e.preventDefault();
return this.searchInput.val('').focus();
- };
+ }
- SearchAutocomplete.prototype.onSearchInputBlur = function(e) {
+ onSearchInputBlur(e) {
this.isFocused = false;
this.wrap.removeClass('search-active');
// If input is blank then restore state
if (this.searchInput.val() === '') {
return this.restoreOriginalState();
}
- };
+ }
- SearchAutocomplete.prototype.addLocationBadge = function(item) {
+ addLocationBadge(item) {
var badgeText, category, value;
category = item.category != null ? item.category + ": " : '';
value = item.value != null ? item.value : '';
badgeText = "" + category + value;
this.locationBadgeEl.text(badgeText).show();
return this.wrap.addClass('has-location-badge');
- };
+ }
- SearchAutocomplete.prototype.hasLocationBadge = function() {
+ hasLocationBadge() {
return this.wrap.is('.has-location-badge');
};
- SearchAutocomplete.prototype.restoreOriginalState = function() {
+ restoreOriginalState() {
var i, input, inputs, len;
inputs = Object.keys(this.originalState);
for (i = 0, len = inputs.length; i < len; i++) {
@@ -320,13 +318,13 @@
value: this.originalState._location
});
}
- };
+ }
- SearchAutocomplete.prototype.badgePresent = function() {
+ badgePresent() {
return this.locationBadgeEl.length;
- };
+ }
- SearchAutocomplete.prototype.resetSearchState = function() {
+ resetSearchState() {
var i, input, inputs, len, results;
inputs = Object.keys(this.originalState);
results = [];
@@ -339,30 +337,30 @@
results.push(this.getElement("#" + input).val(''));
}
return results;
- };
+ }
- SearchAutocomplete.prototype.removeLocationBadge = function() {
+ removeLocationBadge() {
this.locationBadgeEl.hide();
this.resetSearchState();
this.wrap.removeClass('has-location-badge');
return this.disableAutocomplete();
- };
+ }
- SearchAutocomplete.prototype.disableAutocomplete = function() {
+ disableAutocomplete() {
if (!this.searchInput.hasClass('disabled') && this.dropdown.hasClass('open')) {
this.searchInput.addClass('disabled');
this.dropdown.removeClass('open').trigger('hidden.bs.dropdown');
this.restoreMenu();
}
- };
+ }
- SearchAutocomplete.prototype.restoreMenu = function() {
+ restoreMenu() {
var html;
html = "<ul> <li><a class='dropdown-menu-empty-link is-focused'>Loading...</a></li> </ul>";
return this.dropdownContent.html(html);
};
- SearchAutocomplete.prototype.onClick = function(item, $el, e) {
+ onClick(item, $el, e) {
if (location.pathname.indexOf(item.url) !== -1) {
e.preventDefault();
if (!this.badgePresent) {
@@ -385,8 +383,45 @@
}
};
- return SearchAutocomplete;
+ }
+
+ global.SearchAutocomplete = SearchAutocomplete;
+
+ $(function() {
+ var $projectOptionsDataEl = $('.js-search-project-options');
+ var $groupOptionsDataEl = $('.js-search-group-options');
+ var $dashboardOptionsDataEl = $('.js-search-dashboard-options');
+
+ if ($projectOptionsDataEl.length) {
+ gl.projectOptions = gl.projectOptions || {};
+
+ var projectPath = $projectOptionsDataEl.data('project-path');
+
+ gl.projectOptions[projectPath] = {
+ name: $projectOptionsDataEl.data('name'),
+ issuesPath: $projectOptionsDataEl.data('issues-path'),
+ mrPath: $projectOptionsDataEl.data('mr-path')
+ };
+ }
- })();
+ if ($groupOptionsDataEl.length) {
+ gl.groupOptions = gl.groupOptions || {};
+
+ var groupPath = $groupOptionsDataEl.data('group-path');
+
+ gl.groupOptions[groupPath] = {
+ name: $groupOptionsDataEl.data('name'),
+ issuesPath: $groupOptionsDataEl.data('issues-path'),
+ mrPath: $groupOptionsDataEl.data('mr-path')
+ };
+ }
+
+ if ($dashboardOptionsDataEl.length) {
+ gl.dashboardOptions = {
+ issuesPath: $dashboardOptionsDataEl.data('issues-path'),
+ mrPath: $dashboardOptionsDataEl.data('mr-path')
+ };
+ }
+ });
-}).call(this);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/single_file_diff.js b/app/assets/javascripts/single_file_diff.js
index 156b9b8abec..ee6af123268 100644
--- a/app/assets/javascripts/single_file_diff.js
+++ b/app/assets/javascripts/single_file_diff.js
@@ -10,12 +10,13 @@
ERROR_HTML = '<div class="nothing-here-block"><i class="fa fa-warning"></i> Could not load diff</div>';
- COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. Click to expand it.</div>';
+ COLLAPSED_HTML = '<div class="nothing-here-block diff-collapsed">This diff is collapsed. <a class="click-to-expand">Click to expand it.</a></div>';
function SingleFileDiff(file) {
this.file = file;
this.toggleDiff = bind(this.toggleDiff, this);
this.content = $('.diff-content', this.file);
+ this.$toggleIcon = $('.diff-toggle-caret', this.file);
this.diffForPath = this.content.find('[data-diff-for-path]').data('diff-for-path');
this.isOpen = !this.diffForPath;
if (this.diffForPath) {
@@ -23,18 +24,22 @@
this.loadingContent = $(WRAPPER).addClass('loading').html(LOADING_HTML).hide();
this.content = null;
this.collapsedContent.after(this.loadingContent);
+ this.$toggleIcon.addClass('fa-caret-right');
} else {
this.collapsedContent = $(WRAPPER).html(COLLAPSED_HTML).hide();
this.content.after(this.collapsedContent);
+ this.$toggleIcon.addClass('fa-caret-down');
}
- this.collapsedContent.on('click', this.toggleDiff);
- $('.file-title > a', this.file).on('click', this.toggleDiff);
+ $('.file-title, .click-to-expand', this.file).on('click', this.toggleDiff);
}
SingleFileDiff.prototype.toggleDiff = function(e) {
+ var $target = $(e.target);
+ if (!$target.hasClass('file-title') && !$target.hasClass('click-to-expand') && !$target.hasClass('diff-toggle-caret')) return;
this.isOpen = !this.isOpen;
if (!this.isOpen && !this.hasError) {
this.content.hide();
+ this.$toggleIcon.addClass('fa-caret-right').removeClass('fa-caret-down');
this.collapsedContent.show();
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
@@ -42,10 +47,12 @@
} else if (this.content) {
this.collapsedContent.hide();
this.content.show();
+ this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
if (typeof DiffNotesApp !== 'undefined') {
DiffNotesApp.compileComponents();
}
} else {
+ this.$toggleIcon.addClass('fa-caret-down').removeClass('fa-caret-right');
return this.getContentHTML();
}
};
diff --git a/app/assets/javascripts/templates/issuable_template_selector.js.es6 b/app/assets/javascripts/templates/issuable_template_selector.js.es6
index c32ddf80219..2ecf3b18975 100644
--- a/app/assets/javascripts/templates/issuable_template_selector.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selector.js.es6
@@ -1,7 +1,7 @@
/*= require ../blob/template_selector */
((global) => {
- class IssuableTemplateSelector extends TemplateSelector {
+ class IssuableTemplateSelector extends gl.TemplateSelector {
constructor(...args) {
super(...args);
this.projectPath = this.dropdown.data('project-path');
@@ -16,7 +16,7 @@
if (initialQuery.name) this.requestFile(initialQuery);
$('.reset-template', this.dropdown.parent()).on('click', () => {
- if (this.currentTemplate) this.setInputValueToTemplateContent();
+ if (this.currentTemplate) this.setInputValueToTemplateContent(false);
});
}
@@ -26,26 +26,28 @@
this.currentTemplate = currentTemplate;
if (err) return; // Error handled by global AJAX error handler
this.stopLoadingSpinner();
- this.setInputValueToTemplateContent();
+ this.setInputValueToTemplateContent(true);
});
return;
}
- setInputValueToTemplateContent() {
+ setInputValueToTemplateContent(append) {
// `this.requestFileSuccess` sets the value of the description input field
- // to the content of the template selected.
+ // to the content of the template selected. If `append` is true, the
+ // template content will be appended to the previous value of the field,
+ // separated by a blank line if the previous value is non-empty.
if (this.titleInput.val() === '') {
// If the title has not yet been set, focus the title input and
- // skip focusing the description input by setting `true` as the 2nd
- // argument to `requestFileSuccess`.
- this.requestFileSuccess(this.currentTemplate, true);
+ // skip focusing the description input by setting `true` as the
+ // `skipFocus` option to `requestFileSuccess`.
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: true, append});
this.titleInput.focus();
} else {
- this.requestFileSuccess(this.currentTemplate);
+ this.requestFileSuccess(this.currentTemplate, {skipFocus: false, append});
}
return;
}
}
global.IssuableTemplateSelector = IssuableTemplateSelector;
-})(window);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/templates/issuable_template_selectors.js.es6 b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
index bd8cdde033e..4e8247b89e1 100644
--- a/app/assets/javascripts/templates/issuable_template_selectors.js.es6
+++ b/app/assets/javascripts/templates/issuable_template_selectors.js.es6
@@ -1,12 +1,12 @@
((global) => {
class IssuableTemplateSelectors {
- constructor(opts = {}) {
- this.$dropdowns = opts.$dropdowns || $('.js-issuable-selector');
- this.editor = opts.editor || this.initEditor();
+ constructor({ $dropdowns, editor } = {}) {
+ this.$dropdowns = $dropdowns || $('.js-issuable-selector');
+ this.editor = editor || this.initEditor();
this.$dropdowns.each((i, dropdown) => {
- let $dropdown = $(dropdown);
- new IssuableTemplateSelector({
+ const $dropdown = $(dropdown);
+ new gl.IssuableTemplateSelector({
pattern: /(\.md)/,
data: $dropdown.data('data'),
wrapper: $dropdown.closest('.js-issuable-selector-wrap'),
@@ -26,4 +26,4 @@
}
global.IssuableTemplateSelectors = IssuableTemplateSelectors;
-})(window);
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/todos.js b/app/assets/javascripts/todos.js.es6
index 93421649ac7..055228c5df8 100644
--- a/app/assets/javascripts/todos.js
+++ b/app/assets/javascripts/todos.js.es6
@@ -1,34 +1,29 @@
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.Todos = (function() {
- function Todos(opts) {
- var ref;
- if (opts == null) {
- opts = {};
- }
- this.allDoneClicked = bind(this.allDoneClicked, this);
- this.doneClicked = bind(this.doneClicked, this);
- this.el = (ref = opts.el) != null ? ref : $('.js-todos-options');
+((global) => {
+
+ class Todos {
+ constructor({ el } = {}) {
+ this.allDoneClicked = this.allDoneClicked.bind(this);
+ this.doneClicked = this.doneClicked.bind(this);
+ this.el = el || $('.js-todos-options');
this.perPage = this.el.data('perPage');
this.clearListeners();
this.initBtnListeners();
this.initFilters();
}
- Todos.prototype.clearListeners = function() {
+ clearListeners() {
$('.done-todo').off('click');
$('.js-todos-mark-all').off('click');
return $('.todo').off('click');
- };
+ }
- Todos.prototype.initBtnListeners = function() {
+ initBtnListeners() {
$('.done-todo').on('click', this.doneClicked);
$('.js-todos-mark-all').on('click', this.allDoneClicked);
return $('.todo').on('click', this.goToTodoUrl);
- };
+ }
- Todos.prototype.initFilters = function() {
+ initFilters() {
new UsersSelect();
this.initFilterDropdown($('.js-project-search'), 'project_id', ['text']);
this.initFilterDropdown($('.js-type-search'), 'type');
@@ -38,125 +33,117 @@
event.preventDefault();
Turbolinks.visit(this.action + '&' + $(this).serialize());
});
- };
+ }
- Todos.prototype.initFilterDropdown = function($dropdown, fieldName, searchFields) {
+ initFilterDropdown($dropdown, fieldName, searchFields) {
$dropdown.glDropdown({
+ fieldName,
selectable: true,
filterable: searchFields ? true : false,
- fieldName: fieldName,
search: { fields: searchFields },
data: $dropdown.data('data'),
clicked: function() {
return $dropdown.closest('form.filter-form').submit();
}
})
- };
+ }
- Todos.prototype.doneClicked = function(e) {
- var $this;
+ doneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $this = $(e.currentTarget);
- $this.disable();
+ const $target = $(e.currentTarget);
+ $target.disable();
return $.ajax({
type: 'POST',
- url: $this.attr('href'),
+ url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete'
},
- success: (function(_this) {
- return function(data) {
- _this.redirectIfNeeded(data.count);
- _this.clearDone($this.closest('li'));
- return _this.updateBadges(data);
- };
- })(this)
+ success: (data) => {
+ this.redirectIfNeeded(data.count);
+ this.clearDone($target.closest('li'));
+ return this.updateBadges(data);
+ }
});
- };
+ }
- Todos.prototype.allDoneClicked = function(e) {
- var $this;
+ allDoneClicked(e) {
e.preventDefault();
e.stopImmediatePropagation();
- $this = $(e.currentTarget);
- $this.disable();
+ $target = $(e.currentTarget);
+ $target.disable();
return $.ajax({
type: 'POST',
- url: $this.attr('href'),
+ url: $target.attr('href'),
dataType: 'json',
data: {
'_method': 'delete'
},
- success: (function(_this) {
- return function(data) {
- $this.remove();
- $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
- return _this.updateBadges(data);
- };
- })(this)
+ success: (data) => {
+ $target.remove();
+ $('.prepend-top-default').html('<div class="nothing-here-block">You\'re all done!</div>');
+ return this.updateBadges(data);
+ }
});
- };
+ }
- Todos.prototype.clearDone = function($row) {
- var $ul;
- $ul = $row.closest('ul');
+ clearDone($row) {
+ const $ul = $row.closest('ul');
$row.remove();
if (!$ul.find('li').length) {
return $ul.parents('.panel').remove();
}
- };
+ }
- Todos.prototype.updateBadges = function(data) {
+ updateBadges(data) {
$('.todos-pending .badge, .todos-pending-count').text(data.count);
return $('.todos-done .badge').text(data.done_count);
- };
+ }
- Todos.prototype.getTotalPages = function() {
+ getTotalPages() {
return this.el.data('totalPages');
- };
+ }
- Todos.prototype.getCurrentPage = function() {
+ getCurrentPage() {
return this.el.data('currentPage');
- };
+ }
- Todos.prototype.getTodosPerPage = function() {
+ getTodosPerPage() {
return this.el.data('perPage');
- };
+ }
+
+ redirectIfNeeded(total) {
+ const currPages = this.getTotalPages();
+ const currPage = this.getCurrentPage();
- Todos.prototype.redirectIfNeeded = function(total) {
- var currPage, currPages, newPages, pageParams, url;
- currPages = this.getTotalPages();
- currPage = this.getCurrentPage();
// Refresh if no remaining Todos
if (!total) {
- location.reload();
+ window.location.reload();
return;
}
// Do nothing if no pagination
if (!currPages) {
return;
}
- newPages = Math.ceil(total / this.getTodosPerPage());
- // Includes query strings
- url = location.href;
- // If new total of pages is different than we have now
+
+ const newPages = Math.ceil(total / this.getTodosPerPage());
+ let url = location.href;
+
if (newPages !== currPages) {
// Redirect to previous page if there's one available
if (currPages > 1 && currPage === currPages) {
- pageParams = {
+ const pageParams = {
page: currPages - 1
};
url = gl.utils.mergeUrlParams(pageParams, url);
}
return Turbolinks.visit(url);
}
- };
+ }
- Todos.prototype.goToTodoUrl = function(e) {
- var todoLink;
- todoLink = $(this).data('url');
+ goToTodoUrl(e) {
+ const todoLink = $(this).data('url');
if (!todoLink) {
return;
}
@@ -167,10 +154,8 @@
} else {
return Turbolinks.visit(todoLink);
}
- };
-
- return Todos;
-
- })();
+ }
+ }
-}).call(this);
+ global.Todos = Todos;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/user.js.es6 b/app/assets/javascripts/user.js.es6
index 6889d3a7491..0f97924d94e 100644
--- a/app/assets/javascripts/user.js.es6
+++ b/app/assets/javascripts/user.js.es6
@@ -1,7 +1,7 @@
-(global => {
+((global) => {
global.User = class {
- constructor(opts) {
- this.opts = opts;
+ constructor({ action }) {
+ this.action = action;
this.placeProfileAvatarsToTop();
this.initTabs();
this.hideProjectLimitMessage();
@@ -14,9 +14,9 @@
}
initTabs() {
- return new UserTabs({
+ return new global.UserTabs({
parentEl: '.user-profile',
- action: this.opts.action
+ action: this.action
});
}
diff --git a/app/assets/javascripts/user_tabs.js b/app/assets/javascripts/user_tabs.js
deleted file mode 100644
index 8a657780eb6..00000000000
--- a/app/assets/javascripts/user_tabs.js
+++ /dev/null
@@ -1,188 +0,0 @@
-// UserTabs
-//
-// Handles persisting and restoring the current tab selection and lazily-loading
-// content on the Users#show page.
-//
-// ### Example Markup
-//
-// <ul class="nav-links">
-// <li class="activity-tab active">
-// <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
-// Activity
-// </a>
-// </li>
-// <li class="groups-tab">
-// <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
-// Groups
-// </a>
-// </li>
-// <li class="contributed-tab">
-// <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
-// Contributed projects
-// </a>
-// </li>
-// <li class="projects-tab">
-// <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
-// Personal projects
-// </a>
-// </li>
-// <li class="snippets-tab">
-// <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
-// </a>
-// </li>
-// </ul>
-//
-// <div class="tab-content">
-// <div class="tab-pane" id="activity">
-// Activity Content
-// </div>
-// <div class="tab-pane" id="groups">
-// Groups Content
-// </div>
-// <div class="tab-pane" id="contributed">
-// Contributed projects content
-// </div>
-// <div class="tab-pane" id="projects">
-// Projects content
-// </div>
-// <div class="tab-pane" id="snippets">
-// Snippets content
-// </div>
-// </div>
-//
-// <div class="loading-status">
-// <div class="loading">
-// Loading Animation
-// </div>
-// </div>
-//
-(function() {
- var bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; };
-
- this.UserTabs = (function() {
- function UserTabs(opts) {
- this.tabShown = bind(this.tabShown, this);
- var i, item, len, ref, ref1, ref2, ref3;
- this.action = (ref = opts.action) != null ? ref : 'activity', this.defaultAction = (ref1 = opts.defaultAction) != null ? ref1 : 'activity', this.parentEl = (ref2 = opts.parentEl) != null ? ref2 : $(document);
- // Make jQuery object if selector is provided
- if (typeof this.parentEl === 'string') {
- this.parentEl = $(this.parentEl);
- }
- // Store the `location` object, allowing for easier stubbing in tests
- this._location = location;
- // Set tab states
- this.loaded = {};
- ref3 = this.parentEl.find('.nav-links a');
- for (i = 0, len = ref3.length; i < len; i++) {
- item = ref3[i];
- this.loaded[$(item).attr('data-action')] = false;
- }
- // Actions
- this.actions = Object.keys(this.loaded);
- this.bindEvents();
- // Set active tab
- if (this.action === 'show') {
- this.action = this.defaultAction;
- }
- this.activateTab(this.action);
- }
-
- UserTabs.prototype.bindEvents = function() {
- // Toggle event listeners
- return this.parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]').on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', this.tabShown);
- };
-
- UserTabs.prototype.tabShown = function(event) {
- var $target, action, source;
- $target = $(event.target);
- action = $target.data('action');
- source = $target.attr('href');
- this.setTab(source, action);
- return this.setCurrentAction(action);
- };
-
- UserTabs.prototype.activateTab = function(action) {
- return this.parentEl.find(".nav-links .js-" + action + "-tab a").tab('show');
- };
-
- UserTabs.prototype.setTab = function(source, action) {
- if (this.loaded[action] === true) {
- return;
- }
- if (action === 'activity') {
- this.loadActivities(source);
- }
- if (action === 'groups' || action === 'contributed' || action === 'projects' || action === 'snippets') {
- return this.loadTab(source, action);
- }
- };
-
- UserTabs.prototype.loadTab = function(source, action) {
- return $.ajax({
- beforeSend: (function(_this) {
- return function() {
- return _this.toggleLoading(true);
- };
- })(this),
- complete: (function(_this) {
- return function() {
- return _this.toggleLoading(false);
- };
- })(this),
- dataType: 'json',
- type: 'GET',
- url: source + ".json",
- success: (function(_this) {
- return function(data) {
- var tabSelector;
- tabSelector = 'div#' + action;
- _this.parentEl.find(tabSelector).html(data.html);
- _this.loaded[action] = true;
- // Fix tooltips
- return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
- };
- })(this)
- });
- };
-
- UserTabs.prototype.loadActivities = function(source) {
- var $calendarWrap;
- if (this.loaded['activity'] === true) {
- return;
- }
- $calendarWrap = this.parentEl.find('.user-calendar');
- $calendarWrap.load($calendarWrap.data('href'));
- new Activities();
- return this.loaded['activity'] = true;
- };
-
- UserTabs.prototype.toggleLoading = function(status) {
- return this.parentEl.find('.loading-status .loading').toggle(status);
- };
-
- UserTabs.prototype.setCurrentAction = function(action) {
- var new_state, regExp;
- // Remove possible actions from URL
- regExp = new RegExp('\/(' + this.actions.join('|') + ')(\.html)?\/?$');
- new_state = this._location.pathname;
- // remove trailing slashes
- new_state = new_state.replace(/\/+$/, "");
- new_state = new_state.replace(regExp, '');
- // Append the new action if we're on a tab other than 'activity'
- if (action !== this.defaultAction) {
- new_state += "/" + action;
- }
- // Ensure parameters and hash come along for the ride
- new_state += this._location.search + this._location.hash;
- history.replaceState({
- turbolinks: true,
- url: new_state
- }, document.title, new_state);
- return new_state;
- };
-
- return UserTabs;
-
- })();
-
-}).call(this);
diff --git a/app/assets/javascripts/user_tabs.js.es6 b/app/assets/javascripts/user_tabs.js.es6
new file mode 100644
index 00000000000..dfdfa1e7f75
--- /dev/null
+++ b/app/assets/javascripts/user_tabs.js.es6
@@ -0,0 +1,157 @@
+/*
+UserTabs
+
+Handles persisting and restoring the current tab selection and lazily-loading
+content on the Users#show page.
+
+### Example Markup
+
+ <ul class="nav-links">
+ <li class="activity-tab active">
+ <a data-action="activity" data-target="#activity" data-toggle="tab" href="/u/username">
+ Activity
+ </a>
+ </li>
+ <li class="groups-tab">
+ <a data-action="groups" data-target="#groups" data-toggle="tab" href="/u/username/groups">
+ Groups
+ </a>
+ </li>
+ <li class="contributed-tab">
+ <a data-action="contributed" data-target="#contributed" data-toggle="tab" href="/u/username/contributed">
+ Contributed projects
+ </a>
+ </li>
+ <li class="projects-tab">
+ <a data-action="projects" data-target="#projects" data-toggle="tab" href="/u/username/projects">
+ Personal projects
+ </a>
+ </li>
+ <li class="snippets-tab">
+ <a data-action="snippets" data-target="#snippets" data-toggle="tab" href="/u/username/snippets">
+ </a>
+ </li>
+ </ul>
+
+ <div class="tab-content">
+ <div class="tab-pane" id="activity">
+ Activity Content
+ </div>
+ <div class="tab-pane" id="groups">
+ Groups Content
+ </div>
+ <div class="tab-pane" id="contributed">
+ Contributed projects content
+ </div>
+ <div class="tab-pane" id="projects">
+ Projects content
+ </div>
+ <div class="tab-pane" id="snippets">
+ Snippets content
+ </div>
+ </div>
+
+ <div class="loading-status">
+ <div class="loading">
+ Loading Animation
+ </div>
+ </div>
+*/
+((global) => {
+ class UserTabs {
+ constructor ({ defaultAction, action, parentEl }) {
+ this.loaded = {};
+ this.defaultAction = defaultAction || 'activity';
+ this.action = action || this.defaultAction;
+ this.$parentEl = $(parentEl) || $(document);
+ this._location = window.location;
+ this.$parentEl.find('.nav-links a')
+ .each((i, navLink) => {
+ this.loaded[$(navLink).attr('data-action')] = false;
+ });
+ this.actions = Object.keys(this.loaded);
+ this.bindEvents();
+
+ if (this.action === 'show') {
+ this.action = this.defaultAction;
+ }
+
+ this.activateTab(this.action);
+ }
+
+ bindEvents() {
+ return this.$parentEl.off('shown.bs.tab', '.nav-links a[data-toggle="tab"]')
+ .on('shown.bs.tab', '.nav-links a[data-toggle="tab"]', event => this.tabShown(event));
+ }
+
+ tabShown(event) {
+ const $target = $(event.target);
+ const action = $target.data('action');
+ const source = $target.attr('href');
+ this.setTab(source, action);
+ return this.setCurrentAction(source, action);
+ }
+
+ activateTab(action) {
+ return this.$parentEl.find(`.nav-links .js-${action}-tab a`)
+ .tab('show');
+ }
+
+ setTab(source, action) {
+ if (this.loaded[action]) {
+ return;
+ }
+ if (action === 'activity') {
+ this.loadActivities(source);
+ }
+
+ const loadableActions = [ 'groups', 'contributed', 'projects', 'snippets' ];
+ if (loadableActions.indexOf(action) > -1) {
+ return this.loadTab(source, action);
+ }
+ }
+
+ loadTab(source, action) {
+ return $.ajax({
+ beforeSend: () => this.toggleLoading(true),
+ complete: () => this.toggleLoading(false),
+ dataType: 'json',
+ type: 'GET',
+ url: `${source}.json`,
+ success: (data) => {
+ const tabSelector = `div#${action}`;
+ this.$parentEl.find(tabSelector).html(data.html);
+ this.loaded[action] = true;
+ return gl.utils.localTimeAgo($('.js-timeago', tabSelector));
+ }
+ });
+ }
+
+ loadActivities(source) {
+ if (this.loaded['activity']) {
+ return;
+ }
+ const $calendarWrap = this.$parentEl.find('.user-calendar');
+ $calendarWrap.load($calendarWrap.data('href'));
+ new Activities();
+ return this.loaded['activity'] = true;
+ }
+
+ toggleLoading(status) {
+ return this.$parentEl.find('.loading-status .loading')
+ .toggle(status);
+ }
+
+ setCurrentAction(source, action) {
+ let new_state = source
+ new_state = new_state.replace(/\/+$/, '');
+ new_state += this._location.search + this._location.hash;
+ history.replaceState({
+ turbolinks: true,
+ url: new_state
+ }, document.title, new_state);
+ return new_state;
+ }
+ }
+ global.UserTabs = UserTabs;
+})(window.gl || (window.gl = {}));
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index 9c277998db4..bcabda3ceb2 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -14,11 +14,12 @@
$('.js-user-search').each((function(_this) {
return function(i, dropdown) {
var options = {};
- var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser;
+ var $block, $collapsedSidebar, $dropdown, $loading, $selectbox, $value, abilityName, assignTo, assigneeTemplate, collapsedAssigneeTemplate, defaultLabel, firstUser, issueURL, selectedId, showAnyUser, showNullUser, showMenuAbove;
$dropdown = $(dropdown);
options.projectId = $dropdown.data('project-id');
options.showCurrentUser = $dropdown.data('current-user');
showNullUser = $dropdown.data('null-user');
+ showMenuAbove = $dropdown.data('showMenuAbove');
showAnyUser = $dropdown.data('any-user');
firstUser = $dropdown.data('first-user');
options.authorId = $dropdown.data('author-id');
@@ -70,9 +71,10 @@
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
- collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/u/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
- assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/u/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
+ collapsedAssigneeTemplate = _.template('<% if( avatar ) { %> <a class="author_link" href="/<%- username %>"> <img width="24" class="avatar avatar-inline s24" alt="" src="<%- avatar %>"> </a> <% } else { %> <i class="fa fa-user"></i> <% } %>');
+ assigneeTemplate = _.template('<% if (username) { %> <a class="author_link bold" href="/<%- username %>"> <% if( avatar ) { %> <img width="32" class="avatar avatar-inline s32" alt="" src="<%- avatar %>"> <% } %> <span class="author"><%- name %></span> <span class="username"> @<%- username %> </span> </a> <% } else { %> <span class="no-value assign-yourself"> No assignee - <a href="#" class="js-assign-yourself"> assign yourself </a> </span> <% } %>');
return $dropdown.glDropdown({
+ showMenuAbove: showMenuAbove,
data: function(term, callback) {
var isAuthorFilter;
isAuthorFilter = $('.js-author-search');
@@ -116,8 +118,11 @@
if (showDivider) {
users.splice(showDivider, 0, "divider");
}
- // Send the data back
- return callback(users);
+
+ callback(users);
+ if (showMenuAbove) {
+ $dropdown.data('glDropdown').positionMenuAbove();
+ }
});
},
filterable: true,
@@ -127,8 +132,8 @@
},
selectable: true,
fieldName: $dropdown.data('field-name'),
- toggleLabel: function(selected) {
- if (selected && 'id' in selected) {
+ toggleLabel: function(selected, el) {
+ if (selected && 'id' in selected && $(el).hasClass('is-active')) {
if (selected.text) {
return selected.text;
} else {
@@ -138,6 +143,7 @@
return defaultLabel;
}
},
+ defaultLabel: defaultLabel,
inputId: 'issue_assignee_id',
hidden: function(e) {
$selectbox.hide();
@@ -149,7 +155,9 @@
page = $('body').data('page');
isIssueIndex = page === 'projects:issues:index';
isMRIndex = (page === page && page === 'projects:merge_requests:index');
- if ($dropdown.hasClass('js-filter-bulk-update')) {
+ if ($dropdown.hasClass('js-filter-bulk-update') || $dropdown.hasClass('js-issuable-form-dropdown')) {
+ e.preventDefault();
+ selectedId = user.id;
return;
}
if (page === 'projects:boards:show') {
@@ -167,6 +175,9 @@
return assignTo(selected);
}
},
+ id: function (user) {
+ return user.id;
+ },
renderRow: function(user) {
var avatar, img, listClosingTags, listWithName, listWithUserName, selected, username;
username = user.username ? "@" + user.username : "";
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index c79b22d4d21..98e301d3799 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -4,7 +4,7 @@
width: 40px;
height: 40px;
padding: 0;
- @include border-radius($avatar_radius);
+ border-radius: $avatar_radius;
border: 1px solid rgba(0, 0, 0, .1);
&.avatar-inline {
@@ -17,7 +17,7 @@
}
&.avatar-tile {
- @include border-radius(0);
+ border-radius: 0;
border: none;
}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index 2432ddb72f4..8002e56724b 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -19,10 +19,8 @@
&.diff-collapsed {
padding: 5px;
- cursor: pointer;
-
- &:hover {
- background-color: $row-hover;
+ .click-to-expand {
+ cursor: pointer;
}
}
}
@@ -135,7 +133,7 @@
}
.identicon {
- @include border-radius(50%);
+ border-radius: 50%;
}
}
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index 4618687a4be..a7c8d782e9b 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -1,5 +1,5 @@
@mixin btn-default {
- @include border-radius(3px);
+ border-radius: 3px;
font-size: $gl-font-size;
font-weight: 500;
padding: $gl-vert-padding $gl-btn-padding;
@@ -8,7 +8,7 @@
&:active {
outline: none;
background-color: $btn-active-gray;
- @include box-shadow($gl-btn-active-background);
+ box-shadow: $gl-btn-active-background;
}
}
@@ -43,7 +43,7 @@
&:active,
&.active {
- @include box-shadow ($gl-btn-active-background);
+ box-shadow: $gl-btn-active-background;
background-color: $dark;
border-color: $border-dark;
@@ -194,10 +194,17 @@
pointer-events: none !important;
}
- .caret {
+ .fa-caret-down,
+ .fa-caret-up {
margin-left: 5px;
}
+ &.dropdown-toggle {
+ .fa-caret-down {
+ margin-left: 3px;
+ }
+ }
+
svg {
height: 15px;
width: 15px;
@@ -272,7 +279,7 @@
}
.active {
- @include box-shadow($gl-btn-active-background);
+ box-shadow: $gl-btn-active-background;
border: 1px solid #c6cacf !important;
background-color: #e4e7ed !important;
@@ -336,3 +343,9 @@
box-shadow: inset 0 0 0 white;
}
}
+
+@media (max-width: $screen-xs-max) {
+ .btn-wide-on-xs {
+ width: 100%;
+ }
+}
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b0ba112476b..baa95711329 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -1,20 +1,3 @@
-.caret {
- display: inline-block;
- width: 0;
- height: 0;
- margin-left: 2px;
- vertical-align: middle;
- border-top: $caret-width-base dashed;
- border-right: $caret-width-base solid transparent;
- border-left: $caret-width-base solid transparent;
-}
-
-.btn-group {
- .caret {
- margin-left: 0;
- }
-}
-
.dropdown {
position: relative;
@@ -604,3 +587,9 @@
display: block;
color: $gl-placeholder-color;
}
+
+.dropdown-toggle-text {
+ &.is-default {
+ color: $gl-placeholder-color;
+ }
+}
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 76a3c083697..81520500594 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -26,6 +26,15 @@
padding: 10px $gl-padding;
word-wrap: break-word;
border-radius: 3px 3px 0 0;
+ cursor: pointer;
+
+ &:hover {
+ background-color: $dark-background-color;
+ }
+
+ .diff-toggle-caret {
+ padding-right: 6px;
+ }
&.file-title-clear {
padding-left: 0;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 7ae309ba103..a55dcf4a699 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -3,6 +3,8 @@
margin: 0;
margin-bottom: $gl-padding;
font-size: 14px;
+ position: relative;
+ z-index: 1;
.flash-notice {
@extend .alert;
@@ -19,7 +21,8 @@
.flash-notice, .flash-alert {
border-radius: $border-radius-default;
- .container-fluid.container-limited.flash-text {
+ .container-fluid,
+ .container-fluid.container-limited {
background: transparent;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 37ff7e22ed1..05e8ee0190d 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -73,7 +73,7 @@ label {
}
.form-control {
- @include box-shadow(none);
+ box-shadow: none;
border-radius: 3px;
padding: $gl-vert-padding $gl-input-padding;
}
@@ -81,10 +81,10 @@ label {
.select-wrapper {
position: relative;
- .caret {
+ .fa-caret-down {
position: absolute;
right: 10px;
- top: $gl-padding;
+ top: 10px;
color: $gray-darkest;
pointer-events: none;
}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index d4a030f7f7a..9823abdde1f 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -57,6 +57,10 @@ header {
&:hover, &:focus, &:active {
background-color: $background-color;
}
+
+ .fa-caret-down {
+ font-size: 15px;
+ }
}
.navbar-toggle {
@@ -112,11 +116,15 @@ header {
.header-logo {
position: absolute;
left: 50%;
- margin-left: -18px;
top: 7px;
transition-duration: .3s;
z-index: 999;
+ #logo {
+ position: relative;
+ left: -50%;
+ }
+
svg, img {
height: 36px;
}
@@ -126,8 +134,12 @@ header {
}
@media (max-width: $screen-xs-max) {
- right: 25px;
+ right: 20px;
left: auto;
+
+ #logo {
+ left: auto;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 8bfc0d583c5..ba3930e03bd 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -16,7 +16,7 @@
margin-top: 5px;
}
- @include border-radius(3px);
+ border-radius: 3px;
display: block;
float: left;
margin-right: 10px;
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 46af18580d5..efc348214c2 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -164,7 +164,7 @@ ul.content-list {
}
.no-comments {
- opacity: 0.5;
+ opacity: .5;
}
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index edea4ad00eb..6d28d98b283 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -86,7 +86,7 @@
}
.markdown-area {
- @include border-radius(0);
+ border-radius: 0;
background: #fff;
border: 1px solid #ddd;
min-height: 140px;
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 1ec08cdef23..7c207969b0a 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -1,15 +1,4 @@
/**
- * Generic mixins
- */
-@mixin box-shadow($shadow) {
- box-shadow: $shadow;
-}
-
-@mixin border-radius($radius) {
- border-radius: $radius;
-}
-
-/**
* Prefilled mixins
* Mixins with fixed values
*/
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index 76b93b23b95..9fe390eb09d 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -133,5 +133,5 @@
font-size: 20px;
color: #777;
z-index: 100;
- @include box-shadow(0 1px 2px #ddd);
+ box-shadow: 0 1px 2px #ddd;
}
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index c75dacf95d9..79cd26714a3 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -21,7 +21,14 @@
padding-right: 10px;
b {
- @extend .caret;
+ display: inline-block;
+ width: 0;
+ height: 0;
+ margin-left: 2px;
+ vertical-align: middle;
+ border-top: $caret-width-base dashed;
+ border-right: $caret-width-base solid transparent;
+ border-left: $caret-width-base solid transparent;
color: $gray-darkest;
}
}
@@ -39,8 +46,8 @@
}
.select2-drop {
- @include box-shadow(rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0);
- @include border-radius ($border-radius-default);
+ box-shadow: rgba(76, 86, 103, 0.247059) 0 0 1px 0, rgba(31, 37, 50, 0.317647) 0 2px 18px 0;
+ border-radius: $border-radius-default;
border: none;
min-width: 175px;
}
@@ -65,7 +72,7 @@
.select2-container-active {
.select2-choice, .select2-choices {
- @include box-shadow(none);
+ box-shadow: none;
}
}
@@ -75,13 +82,13 @@
outline: 0;
background-image: none;
background-color: $white-dark;
- @include box-shadow($gl-btn-active-gradient);
+ box-shadow: $gl-btn-active-gradient;
}
}
.select2-container-multi {
.select2-choices {
- @include border-radius($border-radius-default);
+ border-radius: $border-radius-default;
border-color: $input-border;
background: none;
@@ -116,7 +123,7 @@
&.select2-container-active .select2-choices,
&.select2-dropdown-open .select2-choices {
border-color: $border-white-normal;
- @include box-shadow($gl-btn-active-gradient);
+ box-shadow: $gl-btn-active-gradient;
}
}
@@ -150,7 +157,7 @@
background-repeat: no-repeat;
background-position: right 0 bottom 6px;
border: 1px solid $input-border;
- @include border-radius($border-radius-default);
+ border-radius: $border-radius-default;
transition: border-color ease-in-out 0.15s, box-shadow ease-in-out 0.15s;
&:focus {
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 3b7de4b57bb..ec52f326eb9 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -4,7 +4,7 @@
&.page-sidebar-pinned {
.sidebar-wrapper {
- @include box-shadow(none);
+ box-shadow: none;
}
}
@@ -17,7 +17,7 @@
width: 0;
overflow: hidden;
transition: width $sidebar-transition-duration;
- @include box-shadow(2px 0 16px 0 $black-transparent);
+ box-shadow: 2px 0 16px 0 $black-transparent;
}
}
@@ -100,7 +100,7 @@
.count {
float: right;
padding: 0 8px;
- @include border-radius(6px);
+ border-radius: 6px;
}
}
@@ -142,6 +142,7 @@
transition-duration: .3s;
position: absolute;
top: 0;
+ cursor: pointer;
&:hover,
&:focus {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 9f2d53d5206..d099a884f54 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -116,7 +116,7 @@
font-size: 13px;
line-height: 1.6em;
overflow-x: auto;
- @include border-radius(2px);
+ border-radius: 2px;
}
p > code {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 14ec310de2d..4c34ed3ebf7 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -17,8 +17,10 @@ $white-normal: #ededed;
$white-dark: #ececec;
$gray-light: #fafafa;
+$gray-lighter: #f9f9f9;
$gray-normal: #f5f5f5;
$gray-dark: #ededed;
+$gray-darker: #eee;
$gray-darkest: #c9c9c9;
$green-light: #38ae67;
@@ -33,6 +35,8 @@ $blue-medium-light: #3498cb;
$blue-medium: #2f8ebf;
$blue-medium-dark: #2d86b4;
+$blue-light-transparent: rgba(44, 159, 216, 0.05);
+
$orange-light: #fc8a51;
$orange-normal: #e75e40;
$orange-dark: #ce5237;
@@ -91,6 +95,7 @@ $table-text-gray: #8f8f8f;
$gl-font-size: 15px;
$gl-title-color: #333;
$gl-text-color: #5c5c5c;
+$gl-text-color-light: #8c8c8c;
$gl-text-green: #4a2;
$gl-text-red: #d12f19;
$gl-text-orange: #d90;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 9c84dceed05..6e81c12aa55 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -162,6 +162,10 @@ lex
list-style: none;
overflow-y: scroll;
overflow-x: hidden;
+
+ &.is-smaller {
+ height: calc(100% - 185px);
+ }
}
.board-list-loading {
@@ -197,6 +201,7 @@ lex
a {
color: inherit;
+ word-wrap: break-word;
}
}
@@ -232,3 +237,31 @@ lex
margin-right: 5px;
}
}
+
+.board-new-issue-form {
+ margin: 5px;
+}
+
+.board-issue-count-holder {
+ margin-top: -3px;
+
+ .btn {
+ line-height: 12px;
+ border-top-left-radius: 0;
+ border-bottom-left-radius: 0;
+ }
+}
+
+.board-issue-count {
+ padding-right: 10px;
+ padding-left: 10px;
+ line-height: 21px;
+ border-radius: $border-radius-base;
+ border: 1px solid $border-color;
+
+ &.has-btn {
+ border-top-right-radius: 0;
+ border-bottom-right-radius: 0;
+ border-width: 1px 0 1px 1px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index a5a260d4c8f..194a39a8377 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -107,10 +107,14 @@
.block {
width: 100%;
- }
- .block-first {
- padding: 5px 16px 11px;
+ &.coverage {
+ padding: 0 16px 11px;
+ }
+
+ .btn-group-justified {
+ margin-top: 5px;
+ }
}
.js-build-variable {
@@ -214,6 +218,9 @@
.build-detail-row {
margin-bottom: 5px;
+ &:last-of-type {
+ margin-bottom: 0;
+ }
}
.build-light-text {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index 778471a34d7..d732008de3d 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -50,7 +50,7 @@
.bordered-box {
border: 1px solid $border-color;
- @include border-radius($border-radius-default);
+ border-radius: $border-radius-default;
}
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index 1aa4e06d975..fcc5f32c738 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -1,7 +1,7 @@
.file-editor {
#editor {
border: none;
- @include border-radius(0);
+ border-radius: 0;
height: 500px;
margin: 0;
padding: 0;
@@ -59,6 +59,7 @@
}
.encoding-selector,
+ .soft-wrap-toggle,
.license-selector,
.gitignore-selector,
.gitlab-ci-yml-selector {
@@ -67,6 +68,24 @@
font-family: $regular_font;
}
+ .soft-wrap-toggle {
+ margin: 0 $btn-side-margin;
+ .soft-wrap {
+ display: block;
+ }
+ .no-wrap {
+ display: none;
+ }
+ &.soft-wrap-active {
+ .soft-wrap {
+ display: none;
+ }
+ .no-wrap {
+ display: block;
+ }
+ }
+ }
+
.gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index d01c60ee6ab..3f19e920166 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -1,4 +1,15 @@
+.environments-container,
+.deployments-container {
+ width: 100%;
+ overflow: auto;
+}
+
.environments {
+ .deployment-column {
+ .avatar {
+ float: none;
+ }
+ }
.commit-title {
margin: 0;
@@ -9,6 +20,7 @@
width: 12px;
}
+ .external-url,
.dropdown-new {
color: $table-text-gray;
}
@@ -21,16 +33,35 @@
}
}
+ .build-link,
.branch-name {
color: $gl-dark-link-color;
}
+
+ .deployment {
+ .build-column {
+
+ .build-link {
+ color: $gl-dark-link-color;
+ }
+
+ .avatar {
+ float: none;
+ }
+ }
+ }
}
.table.builds.environments {
- min-width: 500px;
.icon-container {
width: 20px;
text-align: center;
}
+
+ .branch-commit {
+ .commit-id {
+ margin-right: 0;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 1d00da1266c..789d6237df8 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -91,7 +91,7 @@
float: right;
border: 1px solid #eee;
padding: 5px;
- @include border-radius(5px);
+ border-radius: 5px;
background: $gray-light;
margin-left: 10px;
top: -6px;
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 732dc645c66..185ce970e71 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -57,7 +57,6 @@
}
.groups-header {
-
@media (min-width: $screen-sm-min) {
.nav-links {
width: 35%;
@@ -68,3 +67,38 @@
}
}
}
+
+.groups-empty-state {
+ padding: 50px 100px;
+ overflow: hidden;
+
+ @media (max-width: $screen-md-min) {
+ padding: 50px 0;
+ }
+
+ svg {
+ float: right;
+
+ @media (max-width: $screen-md-min) {
+ float: none;
+ display: block;
+ width: 250px;
+ position: relative;
+ left: 50%;
+ margin-left: -125px;
+ }
+ }
+
+ .text-content {
+ float: left;
+ width: 460px;
+ margin-top: 120px;
+
+ @media (max-width: $screen-md-min) {
+ float: none;
+ margin-top: 60px;
+ width: auto;
+ text-align: center;
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 38c7cd98e41..701c29a3986 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -1,7 +1,7 @@
.suggest-colors {
margin-top: 5px;
a {
- @include border-radius(4px);
+ border-radius: 4px;
width: 30px;
height: 30px;
display: inline-block;
@@ -17,7 +17,7 @@
overflow: hidden;
a {
- @include border-radius(0);
+ border-radius: 0;
width: (100% / 7);
margin-right: 0;
margin-bottom: -5px;
@@ -59,6 +59,13 @@
width: 200px;
margin-bottom: 0;
}
+
+ .label {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ vertical-align: middle;
+ max-width: 100%;
+ }
}
.label-description {
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 403171d4532..a5ca509163d 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -73,12 +73,12 @@
height: auto;
&.top {
- @include border-radius(5px 5px 0 0);
+ border-radius: 5px 5px 0 0;
margin-bottom: 0;
}
&.bottom {
- @include border-radius(0 0 5px 5px);
+ border-radius: 0 0 5px 5px;
border-top: 0;
margin-bottom: 20px;
}
@@ -86,7 +86,7 @@
&.middle {
border-top: 0;
margin-bottom: 0;
- @include border-radius(0);
+ border-radius: 0;
}
&:active, &:focus {
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 926247e5e87..1006b3e62e8 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -6,7 +6,7 @@
background: $background-color;
color: $gl-gray;
border: 1px solid $border-color;
- @include border-radius(2px);
+ border-radius: 2px;
form {
margin-bottom: 0;
@@ -70,7 +70,8 @@
&.ci-success {
color: $gl-success;
- a.environment {
+ a.environment,
+ a.pipeline {
color: inherit;
}
}
@@ -203,6 +204,18 @@
word-break: break-all;
}
+.commits-empty {
+ text-align: center;
+
+ h4 {
+ padding-top: 20px;
+ padding-bottom: 10px;
+ }
+ svg {
+ width: 230px;
+ }
+}
+
.mr-list {
.merge-request {
padding: 10px 15px;
@@ -349,6 +362,10 @@
.issuable-form-select-holder {
display: inline-block;
width: 250px;
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
}
.table-holder {
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 6b865730487..8c2ba3ed58c 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -33,6 +33,7 @@
// Issue title
span a {
color: $gl-text-color;
+ word-wrap: break-word;
}
}
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 54124a3d658..d399f84a2ff 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -334,7 +334,7 @@ ul.notes {
.add-diff-note {
margin-top: -4px;
- @include border-radius(40px);
+ border-radius: 40px;
background: #fff;
padding: 4px;
font-size: 16px;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index b035bfc9f3c..05f59279637 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -22,6 +22,11 @@
.table.builds {
min-width: 1200px;
+
+ .branch-commit {
+ width: 33%;
+ }
+
}
}
@@ -224,9 +229,12 @@
.fa {
color: $table-text-gray;
- margin-right: 6px;
font-size: 14px;
}
+
+ svg, .fa {
+ margin-right: 0;
+ }
}
.btn-remove {
@@ -267,18 +275,8 @@
.toggle-pipeline-btn {
background-color: $gray-dark;
- .caret {
- border-top: none;
- border-bottom: 4px solid;
- }
-
&.graph-collapsed {
background-color: $white-light;
-
- .caret {
- border-bottom: none;
- border-top: 4px solid;
- }
}
}
@@ -305,16 +303,41 @@
.stage-column {
display: inline-block;
vertical-align: top;
- margin-right: 65px;
+
+ &:not(:last-child) {
+ margin-right: 44px;
+ }
+
+ &.left-margin {
+ &:not(:first-child) {
+ margin-left: 44px;
+
+ .left-connector {
+ &::before {
+ content: '';
+ position: absolute;
+ top: 48%;
+ left: -48px;
+ border-top: 2px solid $border-color;
+ width: 48px;
+ height: 1px;
+ }
+ }
+ }
+ }
+
+ &.no-margin {
+ margin: 0;
+ }
li {
list-style: none;
}
.stage-name {
- margin-bottom: 15px;
+ margin: 0 0 15px 10px;
font-weight: bold;
- width: 150px;
+ width: 176px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -323,17 +346,23 @@
.build {
border: 1px solid $border-color;
position: relative;
- padding: 6px 10px;
+ padding: 7px 10px 8px;
border-radius: 30px;
- width: 150px;
+ width: 186px;
margin-bottom: 10px;
+ &:hover {
+ background-color: $gray-lighter;
+ .dropdown-menu-toggle {
+ background-color: transparent;
+ }
+ }
+
&.playable {
- background-color: $gray-light;
svg {
- height: 12px;
- width: 12px;
+ height: 13px;
+ width: 20px;
position: relative;
top: 1px;
@@ -344,10 +373,20 @@
}
.build-content {
- width: 130px;
+ display: -ms-flexbox;
+ display: -webkit-flex;
+ display: flex;
+ width: 164px;
+
+ .ci-status-icon {
+ svg {
+ height: 20px;
+ width: 20px;
+ }
+ }
.ci-status-text {
- width: 110px;
+ width: 135px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
@@ -358,42 +397,53 @@
}
a {
- color: $layout-link-gray;
+ color: $gl-text-color-light;
text-decoration: none;
-
- &:hover {
- .ci-status-text {
- text-decoration: underline;
- }
- }
}
.dropdown-menu-toggle {
border: none;
width: auto;
padding: 0;
- color: $layout-link-gray;
+ color: $gl-text-color-light;
+ flex-grow: 1;
.ci-status-text {
- width: 80px;
+ max-width: 112px;
+ width: auto;
}
}
.grouped-pipeline-dropdown {
padding: 8px 0;
- width: 200px;
+ width: 186px;
left: auto;
- right: -214px;
+ right: -197px;
top: -9px;
+ max-height: 245px;
+ overflow-y: scroll;
+
+ a {
+ color: $gl-text-color;
+ padding: 7px 8px 8px;
- a:hover {
- .ci-status-text {
- text-decoration: none;
+ &:hover {
+ background-color: $blue-light-transparent;
+ border-radius: 3px;
+
+ .ci-status-text {
+ text-decoration: none;
+ }
}
}
+ svg {
+ width: 14px;
+ height: 14px;
+ }
+
.ci-status-text {
- width: 145px;
+ width: 112px;
}
.arrow {
@@ -426,9 +476,10 @@
}
.badge {
- background-color: $gray-dark;
- color: $layout-link-gray;
+ background-color: $gray-darker;
+ color: $gl-text-color-light;
font-weight: normal;
+ margin-left: $btn-xs-side-margin;
}
}
@@ -442,10 +493,10 @@
&::after {
content: '';
position: absolute;
- top: 50%;
- right: -69px;
+ top: 48%;
+ right: -48px;
border-top: 2px solid $border-color;
- width: 69px;
+ width: 48px;
height: 1px;
}
}
@@ -454,25 +505,25 @@
&:not(:first-child) {
&::after, &::before {
content: '';
- top: -47px;
+ top: -49px;
position: absolute;
border-bottom: 2px solid $border-color;
- width: 20px;
- height: 65px;
+ width: 25px;
+ height: 69px;
}
// Right connecting curves
&::after {
- right: -20px;
+ right: -25px;
border-right: 2px solid $border-color;
- border-radius: 0 0 15px;
+ border-radius: 0 0 20px;
}
// Left connecting curves
&::before {
- left: -20px;
+ left: -25px;
border-left: 2px solid $border-color;
- border-radius: 0 0 0 15px;
+ border-radius: 0 0 0 20px;
}
}
@@ -480,7 +531,7 @@
&:nth-child(2) {
&::after, &::before {
height: 29px;
- top: -10px;
+ top: -9px;
}
.curve {
display: block;
@@ -538,20 +589,20 @@
width: 21px;
height: 25px;
position: absolute;
- top: -29px;
+ top: -32px;
border-top: 2px solid $border-color;
}
&::after {
- left: -39px;
+ left: -44px;
border-right: 2px solid $border-color;
- border-radius: 0 15px;
+ border-radius: 0 20px;
}
&::before {
- right: -39px;
+ right: -44px;
border-left: 2px solid $border-color;
- border-radius: 15px 0 0;
+ border-radius: 20px 0 0;
}
}
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index 0fcdaf94a21..c7eac5cf4b9 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -94,7 +94,7 @@
.profile-user-bio {
// Limits the width of the user bio for readability.
max-width: 600px;
- margin: 15px auto 0;
+ margin: 10px auto;
padding: 0 16px;
}
@@ -213,29 +213,22 @@
}
.user-profile {
+
.cover-controls a {
margin-left: 5px;
}
+
.profile-header {
margin: 0 auto;
+
.avatar-holder {
width: 90px;
- display: inline-block;
- }
- .user-info {
- display: inline-block;
- text-align: left;
- vertical-align: middle;
- margin-left: 15px;
- .handle {
- color: $gl-gray-light;
- }
- .member-date {
- margin-bottom: 4px;
- }
+ margin: 0 auto 10px;
}
}
+
@media (max-width: $screen-xs-max) {
+
.cover-block {
padding-top: 20px;
}
@@ -258,10 +251,6 @@
}
}
-.user-profile-nav {
- margin-top: 15px;
-}
-
table.u2f-registrations {
th:not(:last-child), td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index e5859fe7384..f8da0983b77 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -4,7 +4,7 @@
text-align: center;
.preview {
- @include border-radius(4px);
+ border-radius: 4px;
height: 80px;
margin-bottom: 10px;
@@ -47,7 +47,7 @@
width: 160px;
img {
- @include border-radius(4px);
+ border-radius: 4px;
max-width: 100%;
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 8c8c403244e..530fb0c0d05 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -146,7 +146,8 @@
}
.project-repo-btn-group,
- .notification-dropdown {
+ .notification-dropdown,
+ .project-dropdown {
margin-left: 10px;
}
@@ -353,7 +354,7 @@ a.deploy-project-label {
justify-content: flex-start;
.fork-thumbnail {
- @include border-radius($border-radius-base);
+ border-radius: $border-radius-base;
background-color: $white-light;
border: 1px solid $border-white-light;
height: 202px;
@@ -370,7 +371,7 @@ a.deploy-project-label {
background-color: $gray-light;
border: 1px solid $gray-dark;
margin: 0 auto;
- @include border-radius(50%);
+ border-radius: 50%;
i {
font-size: 100px;
color: $gray-dark;
@@ -389,7 +390,7 @@ a.deploy-project-label {
}
img {
- @include border-radius(50%);
+ border-radius: 50%;
max-width: 100px;
}
}
@@ -495,7 +496,7 @@ pre.light-well {
}
.light-well {
- @include border-radius (2px);
+ border-radius: 2px;
color: #5b6169;
font-size: 13px;
@@ -743,6 +744,62 @@ pre.light-well {
.dropdown-menu {
width: 300px;
}
+
+ &.from .compare-dropdown-toggle {
+ width: 237px;
+ }
+
+ &.to .compare-dropdown-toggle {
+ width: 254px;
+ }
+
+ .dropdown-toggle-text {
+ display: block;
+ height: 100%;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ width: 100%;
+ }
+}
+
+.compare-ellipsis {
+ display: inline;
+}
+
+@media (max-width: $screen-xs-max) {
+ .compare-form-group {
+ .input-group {
+ width: 100%;
+
+ & > .compare-dropdown-toggle {
+ width: 100%;
+ }
+ }
+
+ .dropdown-menu {
+ width: 100%;
+ }
+ }
+
+ .compare-switch-container {
+ text-align: center;
+ padding: 0 0 $gl-padding;
+
+ .commits-compare-switch {
+ float: none;
+ }
+ }
+
+ .compare-ellipsis {
+ display: block;
+ text-align: center;
+ padding: 0 0 $gl-padding;
+ }
+
+ .commits-compare-btn {
+ width: 100%;
+ }
}
.clearable-input {
@@ -779,4 +836,4 @@ pre.light-well {
border-top-right-radius: 0;
border-bottom-right-radius: 0;
}
-} \ No newline at end of file
+}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index 436fb00ba2e..e77f9816d8a 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -103,7 +103,7 @@
// Custom dropdown positioning
.dropdown-menu {
- top: 30px;
+ top: 37px;
left: -5px;
padding: 0;
diff --git a/app/assets/stylesheets/pages/snippets.scss b/app/assets/stylesheets/pages/snippets.scss
index 4d5df566d9b..857eb76131a 100644
--- a/app/assets/stylesheets/pages/snippets.scss
+++ b/app/assets/stylesheets/pages/snippets.scss
@@ -36,3 +36,7 @@
float: right;
}
}
+
+.snippet-scope-menu .btn-new {
+ margin-top: 15px;
+}
diff --git a/app/assets/stylesheets/pages/status.scss b/app/assets/stylesheets/pages/status.scss
index 0ee7ceecae5..c05f3d5ff32 100644
--- a/app/assets/stylesheets/pages/status.scss
+++ b/app/assets/stylesheets/pages/status.scss
@@ -4,7 +4,7 @@
margin-right: 10px;
border: 1px solid #eee;
white-space: nowrap;
- @include border-radius(4px);
+ border-radius: 4px;
&:hover {
text-decoration: none;
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 68a5d1ae06c..ea76fe18876 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -51,6 +51,7 @@
-webkit-flex-direction: column;
flex-direction: column;
margin-left: 10px;
+ min-width: 55px;
}
.todo-item {
@@ -120,6 +121,14 @@
}
}
+@media (max-width: $screen-sm-max) {
+ .todos-filters {
+ .dropdown-menu-toggle {
+ width: 135px;
+ }
+ }
+}
+
@media (max-width: $screen-xs-max) {
.todo {
.avatar {
@@ -141,4 +150,14 @@
padding-left: 10px;
}
}
+
+ .todos-filters {
+ .row-content-block {
+ padding-bottom: 50px;
+ }
+
+ .dropdown-menu-toggle {
+ width: 100%;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 7b6577c513e..41ad10f07bd 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -27,7 +27,12 @@
}
.last-commit {
- @include str-truncated(60%);
+ @include str-truncated(506px);
+
+ @media (min-width: $screen-sm-max) and (max-width: $screen-md-max) {
+ @include str-truncated(450px);
+ }
+
}
.commit-history-link-spacer {
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 82055006ac0..762e36ee2e9 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -37,7 +37,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
end
def preview
- @message = broadcast_message_params[:message]
+ @broadcast_message = BroadcastMessage.new(broadcast_message_params)
end
protected
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index aed77d0358a..aa7570cd896 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -10,7 +10,7 @@ class Admin::GroupsController < Admin::ApplicationController
def show
@members = @group.members.order("access_level DESC").page(params[:members_page])
- @requesters = @group.requesters
+ @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@projects = @group.projects.page(params[:projects_page])
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 0d2f4f6eb38..1d963bdf7d5 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -22,7 +22,7 @@ class Admin::ProjectsController < Admin::ApplicationController
end
@project_members = @project.members.page(params[:project_members_page])
- @requesters = @project.requesters
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
end
def transfer
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index bd4ba384b29..b3455e04c29 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -173,7 +173,8 @@ class ApplicationController < ActionController::Base
end
def event_filter
- filters = cookies['event_filter'].split(',') if cookies['event_filter'].present?
+ # Split using comma to maintain backward compatibility Ex/ "filter1,filter2"
+ filters = cookies['event_filter'].split(',')[0] if cookies['event_filter'].present?
@event_filter ||= EventFilter.new(filters)
end
diff --git a/app/controllers/ci/application_controller.rb b/app/controllers/ci/application_controller.rb
deleted file mode 100644
index 5bb7d499cdc..00000000000
--- a/app/controllers/ci/application_controller.rb
+++ /dev/null
@@ -1,7 +0,0 @@
-module Ci
- class ApplicationController < ::ApplicationController
- def self.railtie_helpers_paths
- "app/helpers/ci"
- end
- end
-end
diff --git a/app/controllers/ci/lints_controller.rb b/app/controllers/ci/lints_controller.rb
index e06d12cfce1..3eb485de9db 100644
--- a/app/controllers/ci/lints_controller.rb
+++ b/app/controllers/ci/lints_controller.rb
@@ -1,5 +1,5 @@
module Ci
- class LintsController < ApplicationController
+ class LintsController < ::ApplicationController
before_action :authenticate_user!
def show
@@ -14,6 +14,7 @@ module Ci
@config_processor = Ci::GitlabCiYamlProcessor.new(@content)
@stages = @config_processor.stages
@builds = @config_processor.builds
+ @jobs = @config_processor.jobs
end
rescue
@error = 'Undefined error'
diff --git a/app/controllers/ci/projects_controller.rb b/app/controllers/ci/projects_controller.rb
index aa894fde36b..ff297d6ff13 100644
--- a/app/controllers/ci/projects_controller.rb
+++ b/app/controllers/ci/projects_controller.rb
@@ -1,5 +1,5 @@
module Ci
- class ProjectsController < Ci::ApplicationController
+ class ProjectsController < ::ApplicationController
before_action :project
before_action :no_cache, only: [:badge]
before_action :authorize_read_project!, except: [:badge, :index]
diff --git a/app/controllers/concerns/authenticates_with_two_factor.rb b/app/controllers/concerns/authenticates_with_two_factor.rb
index d5a8a962662..4c497711fc0 100644
--- a/app/controllers/concerns/authenticates_with_two_factor.rb
+++ b/app/controllers/concerns/authenticates_with_two_factor.rb
@@ -23,15 +23,24 @@ module AuthenticatesWithTwoFactor
#
# Returns nil
def prompt_for_two_factor(user)
+ return locked_user_redirect(user) if user.access_locked?
+
session[:otp_user_id] = user.id
setup_u2f_authentication(user)
render 'devise/sessions/two_factor'
end
+ def locked_user_redirect(user)
+ flash.now[:alert] = 'Invalid Login or password'
+ render 'devise/sessions/new'
+ end
+
def authenticate_with_two_factor
user = self.resource = find_user
- if user_params[:otp_attempt].present? && session[:otp_user_id]
+ if user.access_locked?
+ locked_user_redirect(user)
+ elsif user_params[:otp_attempt].present? && session[:otp_user_id]
authenticate_with_two_factor_via_otp(user)
elsif user_params[:device_response].present? && session[:otp_user_id]
authenticate_with_two_factor_via_u2f(user)
@@ -50,8 +59,9 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
+ user.increment_failed_attempts!
flash.now[:alert] = 'Invalid two-factor code.'
- render :two_factor
+ prompt_for_two_factor(user)
end
end
@@ -65,6 +75,7 @@ module AuthenticatesWithTwoFactor
remember_me(user) if user_params[:remember_me] == '1'
sign_in(user)
else
+ user.increment_failed_attempts!
flash.now[:alert] = 'Authentication via U2F device failed.'
prompt_for_two_factor(user)
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index 52682ef9dc9..c13333641d3 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -1,6 +1,5 @@
module MembershipActions
extend ActiveSupport::Concern
- include MembersHelper
def request_access
membershipable.request_access(current_user)
@@ -10,28 +9,23 @@ module MembershipActions
end
def approve_access_request
- @member = membershipable.requesters.find(params[:id])
-
- return render_403 unless can?(current_user, action_member_permission(:update, @member), @member)
-
- @member.accept_request
+ Members::ApproveAccessRequestService.new(membershipable, current_user, params).execute
redirect_to polymorphic_url([membershipable, :members])
end
def leave
- @member = membershipable.members.find_by(user_id: current_user) ||
- membershipable.requesters.find_by(user_id: current_user)
- Members::DestroyService.new(@member, current_user).execute
+ member = Members::DestroyService.new(membershipable, current_user, user_id: current_user.id).
+ execute(:all)
- source_type = @member.real_source_type.humanize(capitalize: false)
+ source_type = membershipable.class.to_s.humanize(capitalize: false)
notice =
- if @member.request?
+ if member.request?
"Your access request to the #{source_type} has been withdrawn."
else
- "You left the \"#{@member.source.human_name}\" #{source_type}."
+ "You left the \"#{membershipable.human_name}\" #{source_type}."
end
- redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
+ redirect_path = member.request? ? member.source : [:dashboard, membershipable.class.to_s.tableize]
redirect_to redirect_path, notice: notice
end
diff --git a/app/controllers/concerns/spammable_actions.rb b/app/controllers/concerns/spammable_actions.rb
index 29e243c66a3..99acd98ae13 100644
--- a/app/controllers/concerns/spammable_actions.rb
+++ b/app/controllers/concerns/spammable_actions.rb
@@ -7,7 +7,7 @@ module SpammableActions
def mark_as_spam
if SpamService.new(spammable).mark_as_spam!
- redirect_to spammable, notice: "#{spammable.class.to_s} was submitted to Akismet successfully."
+ redirect_to spammable, notice: "#{spammable.class} was submitted to Akismet successfully."
else
redirect_to spammable, alert: 'Error with Akismet. Please check the logs for more info.'
end
diff --git a/app/controllers/explore/projects_controller.rb b/app/controllers/explore/projects_controller.rb
index 88a0c18180b..a62c6211372 100644
--- a/app/controllers/explore/projects_controller.rb
+++ b/app/controllers/explore/projects_controller.rb
@@ -21,8 +21,7 @@ class Explore::ProjectsController < Explore::ApplicationController
end
def trending
- @projects = TrendingProjectsFinder.new.execute(current_user)
- @projects = filter_projects(@projects)
+ @projects = filter_projects(Project.trending)
@projects = @projects.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 272164cd0cc..18cd800c619 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -15,7 +15,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
@members = @members.order('access_level DESC').page(params[:page]).per(50)
- @requesters = @group.requesters if can?(current_user, :admin_group, @group)
+ @requesters = AccessRequestsFinder.new(@group).execute(current_user)
@group_member = @group.group_members.new
end
@@ -40,10 +40,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
end
def destroy
- @group_member = @group.members.find_by(id: params[:id]) ||
- @group.requesters.find_by(id: params[:id])
-
- Members::DestroyService.new(@group_member, current_user).execute
+ Members::DestroyService.new(@group, current_user, id: params[:id]).execute(:all)
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
diff --git a/app/controllers/import/gitlab_projects_controller.rb b/app/controllers/import/gitlab_projects_controller.rb
index 7d0eff37635..3ec173abcdb 100644
--- a/app/controllers/import/gitlab_projects_controller.rb
+++ b/app/controllers/import/gitlab_projects_controller.rb
@@ -1,6 +1,5 @@
class Import::GitlabProjectsController < Import::BaseController
before_action :verify_gitlab_project_import_enabled
- before_action :authenticate_admin!
def new
@namespace_id = project_params[:namespace_id]
@@ -48,8 +47,4 @@ class Import::GitlabProjectsController < Import::BaseController
:path, :namespace_id, :file
)
end
-
- def authenticate_admin!
- render_404 unless current_user.is_admin?
- end
end
diff --git a/app/controllers/jwt_controller.rb b/app/controllers/jwt_controller.rb
index 34d5d99558e..7e4da73bc11 100644
--- a/app/controllers/jwt_controller.rb
+++ b/app/controllers/jwt_controller.rb
@@ -25,7 +25,7 @@ class JwtController < ApplicationController
authenticate_with_http_basic do |login, password|
@authentication_result = Gitlab::Auth.find_for_git_client(login, password, project: nil, ip: request.ip)
- render_403 unless @authentication_result.success? &&
+ render_unauthorized unless @authentication_result.success? &&
(@authentication_result.actor.nil? || @authentication_result.actor.is_a?(User))
end
rescue Gitlab::Auth::MissingPersonalTokenError
@@ -33,10 +33,21 @@ class JwtController < ApplicationController
end
def render_missing_personal_token
- render plain: "HTTP Basic: Access denied\n" \
- "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
- "You can generate one at #{profile_personal_access_tokens_url}",
- status: 401
+ render json: {
+ errors: [
+ { code: 'UNAUTHORIZED',
+ message: "HTTP Basic: Access denied\n" \
+ "You have 2FA enabled, please use a personal access token for Git over HTTP.\n" \
+ "You can generate one at #{profile_personal_access_tokens_url}" }
+ ] }, status: 401
+ end
+
+ def render_unauthorized
+ render json: {
+ errors: [
+ { code: 'UNAUTHORIZED',
+ message: 'HTTP Basic: Access denied' }
+ ] }, status: 401
end
def auth_params
diff --git a/app/controllers/namespaces_controller.rb b/app/controllers/namespaces_controller.rb
deleted file mode 100644
index 83eec1bf4a2..00000000000
--- a/app/controllers/namespaces_controller.rb
+++ /dev/null
@@ -1,25 +0,0 @@
-class NamespacesController < ApplicationController
- skip_before_action :authenticate_user!
-
- def show
- namespace = Namespace.find_by(path: params[:id])
-
- if namespace
- if namespace.is_a?(Group)
- group = namespace
- else
- user = namespace.owner
- end
- end
-
- if user
- redirect_to user_path(user)
- elsif group && can?(current_user, :read_group, group)
- redirect_to group_path(group)
- elsif current_user.nil?
- authenticate_user!
- else
- render_404
- end
- end
-end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index c5fa756d02b..f71e0a1302b 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -73,7 +73,8 @@ class ProfilesController < Profiles::ApplicationController
:skype,
:twitter,
:username,
- :website_url
+ :website_url,
+ :organization
)
end
end
diff --git a/app/controllers/projects/boards/issues_controller.rb b/app/controllers/projects/boards/issues_controller.rb
index 9404612a993..095af6c35eb 100644
--- a/app/controllers/projects/boards/issues_controller.rb
+++ b/app/controllers/projects/boards/issues_controller.rb
@@ -2,6 +2,7 @@ module Projects
module Boards
class IssuesController < Boards::ApplicationController
before_action :authorize_read_issue!, only: [:index]
+ before_action :authorize_create_issue!, only: [:create]
before_action :authorize_update_issue!, only: [:update]
def index
@@ -9,16 +10,23 @@ module Projects
issues = issues.page(params[:page])
render json: {
- issues: issues.as_json(
- only: [:iid, :title, :confidential],
- include: {
- assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
- labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
- }),
+ issues: serialize_as_json(issues),
size: issues.total_count
}
end
+ def create
+ list = project.board.lists.find(params[:list_id])
+ service = ::Boards::Issues::CreateService.new(project, current_user, issue_params)
+ issue = service.execute(list)
+
+ if issue.valid?
+ render json: serialize_as_json(issue)
+ else
+ render json: issue.errors, status: :unprocessable_entity
+ end
+ end
+
def update
service = ::Boards::Issues::MoveService.new(project, current_user, move_params)
@@ -33,7 +41,7 @@ module Projects
def issue
@issue ||=
- IssuesFinder.new(current_user, project_id: project.id, state: 'all')
+ IssuesFinder.new(current_user, project_id: project.id)
.execute
.where(iid: params[:id])
.first!
@@ -43,6 +51,10 @@ module Projects
return render_403 unless can?(current_user, :read_issue, project)
end
+ def authorize_create_issue!
+ return render_403 unless can?(current_user, :admin_issue, project)
+ end
+
def authorize_update_issue!
return render_403 unless can?(current_user, :update_issue, issue)
end
@@ -54,6 +66,19 @@ module Projects
def move_params
params.permit(:id, :from_list_id, :to_list_id)
end
+
+ def issue_params
+ params.require(:issue).permit(:title).merge(request: request)
+ end
+
+ def serialize_as_json(resource)
+ resource.as_json(
+ only: [:iid, :title, :confidential],
+ include: {
+ assignee: { only: [:id, :name, :username], methods: [:avatar_url] },
+ labels: { only: [:id, :title, :description, :color, :priority], methods: [:text_color] }
+ })
+ end
end
end
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 33206717089..0035633b774 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -1,4 +1,6 @@
class Projects::BoardsController < Projects::ApplicationController
+ include IssuableCollections
+
respond_to :html
before_action :authorize_read_board!, only: [:show]
diff --git a/app/controllers/projects/commit_controller.rb b/app/controllers/projects/commit_controller.rb
index 02fb3f56890..cdfc1ba7b92 100644
--- a/app/controllers/projects/commit_controller.rb
+++ b/app/controllers/projects/commit_controller.rb
@@ -10,10 +10,11 @@ class Projects::CommitController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_download_code!, except: [:cancel_builds, :retry_builds]
before_action :authorize_update_build!, only: [:cancel_builds, :retry_builds]
+ before_action :authorize_read_pipeline!, only: [:pipelines]
before_action :authorize_read_commit_status!, only: [:builds]
before_action :commit
- before_action :define_commit_vars, only: [:show, :diff_for_path, :builds]
- before_action :define_status_vars, only: [:show, :builds]
+ before_action :define_commit_vars, only: [:show, :diff_for_path, :builds, :pipelines]
+ before_action :define_status_vars, only: [:show, :builds, :pipelines]
before_action :define_note_vars, only: [:show, :diff_for_path]
before_action :authorize_edit_tree!, only: [:revert, :cherry_pick]
@@ -31,6 +32,9 @@ class Projects::CommitController < Projects::ApplicationController
render_diff_for_path(@commit.diffs(diff_options))
end
+ def pipelines
+ end
+
def builds
end
@@ -96,10 +100,6 @@ class Projects::CommitController < Projects::ApplicationController
@noteable = @commit ||= @project.commit(params[:id])
end
- def pipelines
- @pipelines ||= project.pipelines.where(sha: commit.sha)
- end
-
def ci_builds
@ci_builds ||= Ci::Build.where(pipeline: pipelines)
end
@@ -134,8 +134,9 @@ class Projects::CommitController < Projects::ApplicationController
end
def define_status_vars
- @statuses = CommitStatus.where(pipeline: pipelines).relevant
- @builds = Ci::Build.where(pipeline: pipelines).relevant
+ @ci_pipelines = project.pipelines.where(sha: commit.sha)
+ @statuses = CommitStatus.where(pipeline: @ci_pipelines).relevant
+ @builds = Ci::Build.where(pipeline: @ci_pipelines).relevant
end
def assign_change_commit_vars(mr_source_branch)
diff --git a/app/controllers/projects/group_links_controller.rb b/app/controllers/projects/group_links_controller.rb
index d0c4550733c..7a7475a7345 100644
--- a/app/controllers/projects/group_links_controller.rb
+++ b/app/controllers/projects/group_links_controller.rb
@@ -4,17 +4,25 @@ class Projects::GroupLinksController < Projects::ApplicationController
def index
@group_links = project.project_group_links.all
+
+ @skip_groups = @group_links.pluck(:group_id)
+ @skip_groups << project.group.try(:id)
end
def create
- group = Group.find(params[:link_group_id])
- return render_404 unless can?(current_user, :read_group, group)
-
- project.project_group_links.create(
- group: group,
- group_access: params[:link_group_access],
- expires_at: params[:expires_at]
- )
+ group = Group.find(params[:link_group_id]) if params[:link_group_id].present?
+
+ if group
+ return render_404 unless can?(current_user, :read_group, group)
+
+ project.project_group_links.create(
+ group: group,
+ group_access: params[:link_group_access],
+ expires_at: params[:expires_at]
+ )
+ else
+ flash[:alert] = 'Please select a group.'
+ end
redirect_to namespace_project_group_links_path(project.namespace, project)
end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 3eb13a121bf..96041b07647 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -54,7 +54,7 @@ class Projects::IssuesController < Projects::ApplicationController
end
def show
- raw_notes = @issue.notes_with_associations.fresh
+ raw_notes = @issue.notes.inc_relations_for_view.fresh
@notes = Banzai::NoteRenderer.
render(raw_notes, @project, current_user, @path, @project_wiki, @ref)
@@ -159,7 +159,8 @@ class Projects::IssuesController < Projects::ApplicationController
protected
def issue
- @noteable = @issue ||= @project.issues.find_by(iid: params[:id]) || redirect_old
+ # The Sortable default scope causes performance issues when used with find_by
+ @noteable = @issue ||= @project.issues.where(iid: params[:id]).reorder(nil).take || redirect_old
end
alias_method :subscribable_resource, :issue
alias_method :issuable, :issue
diff --git a/app/controllers/projects/labels_controller.rb b/app/controllers/projects/labels_controller.rb
index 28fa4a5b141..a6626df4826 100644
--- a/app/controllers/projects/labels_controller.rb
+++ b/app/controllers/projects/labels_controller.rb
@@ -30,9 +30,15 @@ class Projects::LabelsController < Projects::ApplicationController
@label = @project.labels.create(label_params)
if @label.valid?
- redirect_to namespace_project_labels_path(@project.namespace, @project)
+ respond_to do |format|
+ format.html { redirect_to namespace_project_labels_path(@project.namespace, @project) }
+ format.json { render json: @label }
+ end
else
- render 'new'
+ respond_to do |format|
+ format.html { render 'new' }
+ format.json { render json: { message: @label.errors.messages }, status: 400 }
+ end
end
end
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 935417d4ae8..ffd9833e3b1 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -18,6 +18,9 @@ class Projects::MergeRequestsController < Projects::ApplicationController
before_action :define_commit_vars, only: [:diffs]
before_action :define_diff_comment_vars, only: [:diffs]
before_action :ensure_ref_fetched, only: [:show, :diffs, :commits, :builds, :conflicts, :pipelines]
+ before_action :close_merge_request_without_source_project, only: [:show, :diffs, :commits, :builds, :pipelines]
+ before_action :apply_diff_view_cookie!, only: [:new_diffs]
+ before_action :build_merge_request, only: [:new, :new_diffs]
# Allow read any merge_request
before_action :authorize_read_merge_request!
@@ -209,29 +212,26 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def new
- apply_diff_view_cookie!
-
- build_merge_request
- @noteable = @merge_request
-
- @target_branches = if @merge_request.target_project
- @merge_request.target_project.repository.branch_names
- else
- []
- end
-
- @target_project = merge_request.target_project
- @source_project = merge_request.source_project
- @commits = @merge_request.compare_commits.reverse
- @commit = @merge_request.diff_head_commit
- @base_commit = @merge_request.diff_base_commit
- @diffs = @merge_request.diffs(diff_options) if @merge_request.compare
- @diff_notes_disabled = true
- @pipeline = @merge_request.pipeline
- @statuses = @pipeline.statuses.relevant if @pipeline
+ define_new_vars
+ end
- @note_counts = Note.where(commit_id: @commits.map(&:id)).
- group(:commit_id).count
+ def new_diffs
+ respond_to do |format|
+ format.html do
+ define_new_vars
+ render "new"
+ end
+ format.json do
+ @diffs = if @merge_request.can_be_created
+ @merge_request.diffs(diff_options)
+ else
+ []
+ end
+ @diff_notes_disabled = true
+
+ render json: { html: view_to_html_string('projects/merge_requests/_new_diffs', diffs: @diffs) }
+ end
+ end
end
def create
@@ -275,7 +275,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def remove_wip
- MergeRequests::UpdateService.new(project, current_user, title: @merge_request.wipless_title).execute(@merge_request)
+ MergeRequests::UpdateService.new(project, current_user, wip_event: 'unwip').execute(@merge_request)
redirect_to namespace_project_merge_request_path(@project.namespace, @project, @merge_request),
notice: "The merge request can now be merged."
@@ -308,8 +308,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
return
end
- TodoService.new.merge_merge_request(merge_request, current_user)
-
@merge_request.update(merge_error: nil)
if params[:merge_when_build_succeeds].present?
@@ -418,10 +416,6 @@ class Projects::MergeRequestsController < Projects::ApplicationController
end
def validates_merge_request
- # If source project was removed and merge request for some reason
- # wasn't close (Ex. mr from fork to origin)
- return invalid_mr if !@merge_request.source_project && @merge_request.open?
-
# Show git not found page
# if there is no saved commits between source & target branch
if @merge_request.commits.blank?
@@ -495,8 +489,29 @@ class Projects::MergeRequestsController < Projects::ApplicationController
)
end
+ def define_new_vars
+ @noteable = @merge_request
+
+ @target_branches = if @merge_request.target_project
+ @merge_request.target_project.repository.branch_names
+ else
+ []
+ end
+
+ @target_project = merge_request.target_project
+ @source_project = merge_request.source_project
+ @commits = @merge_request.compare_commits.reverse
+ @commit = @merge_request.diff_head_commit
+ @base_commit = @merge_request.diff_base_commit
+
+ @pipeline = @merge_request.pipeline
+ @statuses = @pipeline.statuses.relevant if @pipeline
+ @note_counts = Note.where(commit_id: @commits.map(&:id)).
+ group(:commit_id).count
+ end
+
def invalid_mr
- # Render special view for MR with removed source or target branch
+ # Render special view for MR with removed target branch
render 'invalid'
end
@@ -526,7 +541,7 @@ class Projects::MergeRequestsController < Projects::ApplicationController
def build_merge_request
params[:merge_request] ||= ActionController::Parameters.new(source_project: @project)
- @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params).execute
+ @merge_request = MergeRequests::BuildService.new(project, current_user, merge_request_params.merge(diff_options: diff_options)).execute
end
def compared_diff_version
@@ -538,4 +553,10 @@ class Projects::MergeRequestsController < Projects::ApplicationController
@diff_notes_disabled = !@merge_request_diff.latest?
@diffs = @merge_request_diff.diffs(diff_options)
end
+
+ def close_merge_request_without_source_project
+ if !@merge_request.source_project && @merge_request.open?
+ @merge_request.close
+ end
+ end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 42a7e5a2c30..f56b256984b 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -29,7 +29,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_members = @group_members.order('access_level DESC')
end
- @requesters = @project.requesters if can?(current_user, :admin_project, @project)
+ @requesters = AccessRequestsFinder.new(@project).execute(current_user)
@project_member = @project.project_members.new
@project_group_links = @project.project_group_links
@@ -55,10 +55,8 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
def destroy
- @project_member = @project.members.find_by(id: params[:id]) ||
- @project.requesters.find_by(id: params[:id])
-
- Members::DestroyService.new(@project_member, current_user).execute
+ Members::DestroyService.new(@project, current_user, params).
+ execute(:all)
respond_to do |format|
format.html do
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index eaa38fa6c98..62916270172 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -137,10 +137,10 @@ class ProjectsController < Projects::ApplicationController
noteable =
case params[:type]
when 'Issue'
- IssuesFinder.new(current_user, project_id: @project.id, state: 'all').
+ IssuesFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'MergeRequest'
- MergeRequestsFinder.new(current_user, project_id: @project.id, state: 'all').
+ MergeRequestsFinder.new(current_user, project_id: @project.id).
execute.find_by(iid: params[:type_id])
when 'Commit'
@project.commit(params[:type_id])
@@ -324,7 +324,12 @@ class ProjectsController < Projects::ApplicationController
end
def repo_exists?
- project.repository_exists? && !project.empty_repo?
+ project.repository_exists? && !project.empty_repo? && project.repo
+
+ rescue Gitlab::Git::Repository::NoRepository
+ project.repository.expire_exists_cache
+
+ false
end
def project_view_files?
diff --git a/app/controllers/users_controller.rb b/app/controllers/users_controller.rb
index a4bedb3bfe6..838ecc837e4 100644
--- a/app/controllers/users_controller.rb
+++ b/app/controllers/users_controller.rb
@@ -65,7 +65,7 @@ class UsersController < ApplicationController
format.html { render 'show' }
format.json do
render json: {
- html: view_to_html_string("snippets/_snippets", collection: @snippets)
+ html: view_to_html_string("snippets/_snippets", collection: @snippets, remote: true)
}
end
end
diff --git a/app/finders/access_requests_finder.rb b/app/finders/access_requests_finder.rb
new file mode 100644
index 00000000000..b6ee49df99b
--- /dev/null
+++ b/app/finders/access_requests_finder.rb
@@ -0,0 +1,27 @@
+class AccessRequestsFinder
+ attr_accessor :source
+
+ # Arguments:
+ # source - a Group or Project
+ def initialize(source)
+ @source = source
+ end
+
+ def execute(*args)
+ execute!(*args)
+ rescue Gitlab::Access::AccessDeniedError
+ []
+ end
+
+ def execute!(current_user)
+ raise Gitlab::Access::AccessDeniedError unless can_see_access_requests?(current_user)
+
+ source.requesters
+ end
+
+ private
+
+ def can_see_access_requests?(current_user)
+ source && Ability.allowed?(current_user, :"admin_#{source.class.to_s.underscore}", source)
+ end
+end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index 8f9ef8f725c..9f170428100 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -183,17 +183,12 @@ class IssuableFinder
end
def by_state(items)
- case params[:state]
- when 'closed'
- items.closed
- when 'merged'
- items.respond_to?(:merged) ? items.merged : items.closed
- when 'all'
- items
- when 'opened'
- items.opened
+ params[:state] ||= 'all'
+
+ if items.respond_to?(params[:state])
+ items.public_send(params[:state])
else
- raise 'You must specify default state'
+ items
end
end
diff --git a/app/finders/trending_projects_finder.rb b/app/finders/trending_projects_finder.rb
deleted file mode 100644
index 81a12403801..00000000000
--- a/app/finders/trending_projects_finder.rb
+++ /dev/null
@@ -1,11 +0,0 @@
-class TrendingProjectsFinder
- def execute(current_user, start_date = 1.month.ago)
- projects_for(current_user).trending(start_date)
- end
-
- private
-
- def projects_for(current_user)
- ProjectsFinder.new.execute(current_user)
- end
-end
diff --git a/app/helpers/appearances_helper.rb b/app/helpers/appearances_helper.rb
index de13e7a1fc2..16136d02530 100644
--- a/app/helpers/appearances_helper.rb
+++ b/app/helpers/appearances_helper.rb
@@ -16,7 +16,7 @@ module AppearancesHelper
end
def brand_text
- markdown(brand_item.description)
+ markdown_field(brand_item, :description)
end
def brand_item
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 1df430e6279..ebd78bf9888 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -280,32 +280,6 @@ module ApplicationHelper
end
end
- def state_filters_text_for(entity, project)
- titles = {
- opened: "Open"
- }
-
- entity_title = titles[entity] || entity.to_s.humanize
-
- count =
- if project.nil?
- nil
- elsif current_controller?(:issues)
- project.issues.visible_to_user(current_user).send(entity).count
- elsif current_controller?(:merge_requests)
- project.merge_requests.send(entity).count
- end
-
- html = content_tag :span, entity_title
-
- if count.present?
- html += " "
- html += content_tag :span, number_with_delimiter(count), class: 'badge'
- end
-
- html.html_safe
- end
-
def truncate_first_line(message, length = 50)
truncate(message.each_line.first.chomp, length: length) if message
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 6de25bea654..6229384817b 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -11,18 +11,6 @@ module ApplicationSettingsHelper
current_application_settings.signin_enabled?
end
- def extra_sign_in_text
- current_application_settings.sign_in_text
- end
-
- def after_sign_up_text
- current_application_settings.after_sign_up_text
- end
-
- def shared_runners_text
- current_application_settings.shared_runners_text
- end
-
def user_oauth_applications?
current_application_settings.user_oauth_applications
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index df41473543b..b7e0ff8ecd0 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -4,15 +4,18 @@ module AvatarsHelper
user: commit_or_event.author,
user_name: commit_or_event.author_name,
user_email: commit_or_event.author_email,
+ css_class: 'hidden-xs'
}))
end
def user_avatar(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
+ css_class = options[:css_class] || ''
+
avatar = image_tag(
avatar_icon(options[:user] || options[:user_email], avatar_size),
- class: "avatar has-tooltip hidden-xs s#{avatar_size}",
+ class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar",
title: user_name,
data: { container: 'body' }
diff --git a/app/helpers/broadcast_messages_helper.rb b/app/helpers/broadcast_messages_helper.rb
index 43a29c96bca..eb03ced67eb 100644
--- a/app/helpers/broadcast_messages_helper.rb
+++ b/app/helpers/broadcast_messages_helper.rb
@@ -3,7 +3,7 @@ module BroadcastMessagesHelper
return unless message.present?
content_tag :div, class: 'broadcast-message', style: broadcast_message_style(message) do
- icon('bullhorn') << ' ' << render_broadcast_message(message.message)
+ icon('bullhorn') << ' ' << render_broadcast_message(message)
end
end
@@ -32,7 +32,7 @@ module BroadcastMessagesHelper
end
end
- def render_broadcast_message(message)
- Banzai.render(message, pipeline: :broadcast_message).html_safe
+ def render_broadcast_message(broadcast_message)
+ Banzai.render_field(broadcast_message, :message).html_safe
end
end
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index b478580978b..a695aceea76 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -15,10 +15,11 @@ module ButtonHelper
#
# See http://clipboardjs.com/#usage
def clipboard_button(data = {})
+ css_class = data[:class] || 'btn-clipboard'
data = { toggle: 'tooltip', placement: 'bottom', container: 'body' }.merge(data)
content_tag :button,
icon('clipboard'),
- class: "btn btn-clipboard",
+ class: "btn #{css_class}",
data: data,
type: :button,
title: "Copy to Clipboard"
diff --git a/app/helpers/ci_status_helper.rb b/app/helpers/ci_status_helper.rb
index 639deb7c521..b7f48630bd4 100644
--- a/app/helpers/ci_status_helper.rb
+++ b/app/helpers/ci_status_helper.rb
@@ -56,7 +56,7 @@ module CiStatusHelper
def render_commit_status(commit, tooltip_placement: 'auto left')
project = commit.project
- path = builds_namespace_project_commit_path(project.namespace, project, commit)
+ path = pipelines_namespace_project_commit_path(project.namespace, project, commit)
render_status_with_link('commit', commit.status, path, tooltip_placement: tooltip_placement)
end
diff --git a/app/helpers/dropdowns_helper.rb b/app/helpers/dropdowns_helper.rb
index 4566f3782cc..81e0b6bb5ae 100644
--- a/app/helpers/dropdowns_helper.rb
+++ b/app/helpers/dropdowns_helper.rb
@@ -40,8 +40,9 @@ module DropdownsHelper
end
def dropdown_toggle(toggle_text, data_attr, options = {})
+ default_label = data_attr[:default_label]
content_tag(:button, class: "dropdown-menu-toggle #{options[:toggle_class] if options.has_key?(:toggle_class)}", id: (options[:id] if options.has_key?(:id)), type: "button", data: data_attr) do
- output = content_tag(:span, toggle_text, class: "dropdown-toggle-text")
+ output = content_tag(:span, toggle_text, class: "dropdown-toggle-text #{'is-default' if toggle_text == default_label}")
output << icon('chevron-down')
output.html_safe
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 1a259656f31..0772d848289 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -13,14 +13,12 @@ module GitlabMarkdownHelper
def link_to_gfm(body, url, html_options = {})
return "" if body.blank?
- escaped_body = if body.start_with?('<img')
- body
- else
- escape_once(body)
- end
-
- user = current_user if defined?(current_user)
- gfm_body = Banzai.render(escaped_body, project: @project, current_user: user, pipeline: :single_line)
+ context = {
+ project: @project,
+ current_user: (current_user if defined?(current_user)),
+ pipeline: :single_line,
+ }
+ gfm_body = Banzai.render(body, context)
fragment = Nokogiri::HTML::DocumentFragment.parse(gfm_body)
if fragment.children.size == 1 && fragment.children[0].name == 'a'
@@ -51,17 +49,15 @@ module GitlabMarkdownHelper
context[:project] ||= @project
html = Banzai.render(text, context)
+ banzai_postprocess(html, context)
+ end
- context.merge!(
- current_user: (current_user if defined?(current_user)),
-
- # RelativeLinkFilter
- requested_path: @path,
- project_wiki: @project_wiki,
- ref: @ref
- )
+ def markdown_field(object, field)
+ object = object.for_display if object.respond_to?(:for_display)
+ return "" unless object.present?
- Banzai.post_process(html, context)
+ html = Banzai.render_field(object, field)
+ banzai_postprocess(html, object.banzai_render_context(field))
end
def asciidoc(text)
@@ -196,4 +192,18 @@ module GitlabMarkdownHelper
icon(options[:icon])
end
end
+
+ # Calls Banzai.post_process with some common context options
+ def banzai_postprocess(html, context)
+ context.merge!(
+ current_user: (current_user if defined?(current_user)),
+
+ # RelativeLinkFilter
+ requested_path: @path,
+ project_wiki: @project_wiki,
+ ref: @ref
+ )
+
+ Banzai.post_process(html, context)
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 5c04bba323f..692fadd505f 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -8,18 +8,12 @@ module IssuablesHelper
end
def multi_label_name(current_labels, default_label)
- # current_labels may be a string from before
- if current_labels.is_a?(Array)
- if current_labels.count > 1
- "#{current_labels[0]} +#{current_labels.count - 1} more"
+ if current_labels && current_labels.any?
+ title = current_labels.first.try(:title)
+ if current_labels.size > 1
+ "#{title} +#{current_labels.size - 1} more"
else
- current_labels[0]
- end
- elsif current_labels.is_a?(String)
- if current_labels.nil? || current_labels.empty?
- default_label
- else
- current_labels
+ title
end
else
default_label
@@ -94,6 +88,24 @@ module IssuablesHelper
label_names.join(', ')
end
+ def issuables_state_counter_text(issuable_type, state)
+ titles = {
+ opened: "Open"
+ }
+
+ state_title = titles[state] || state.to_s.humanize
+
+ count =
+ Rails.cache.fetch(issuables_state_counter_cache_key(issuable_type, state), expires_in: 2.minutes) do
+ issuables_count_for_state(issuable_type, state)
+ end
+
+ html = content_tag(:span, state_title)
+ html << " " << content_tag(:span, number_with_delimiter(count), class: 'badge')
+
+ html.html_safe
+ end
+
private
def sidebar_gutter_collapsed?
@@ -111,4 +123,22 @@ module IssuablesHelper
issuable.open? ? :opened : :closed
end
end
+
+ def issuables_count_for_state(issuable_type, state)
+ issuables_finder = public_send("#{issuable_type}_finder")
+ issuables_finder.params[:state] = state
+
+ issuables_finder.execute.page(1).total_count
+ end
+
+ IRRELEVANT_PARAMS_FOR_CACHE_KEY = %i[utf8 sort page]
+ private_constant :IRRELEVANT_PARAMS_FOR_CACHE_KEY
+
+ def issuables_state_counter_cache_key(issuable_type, state)
+ opts = params.with_indifferent_access
+ opts[:state] = state
+ opts.except!(*IRRELEVANT_PARAMS_FOR_CACHE_KEY)
+
+ hexdigest(['issuables_count', issuable_type, opts.sort].flatten.join('-'))
+ end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 8b212b0327a..1644c346dd8 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -113,14 +113,13 @@ module IssuesHelper
end
end
- def award_user_list(awards, current_user)
+ def award_user_list(awards, current_user, limit: 10)
names = awards.map do |award|
award.user == current_user ? 'You' : award.user.name
end
- # Take first 9 OR current user + first 9
current_user_name = names.delete('You')
- names = names.first(9).insert(0, current_user_name).compact
+ names = names.insert(0, current_user_name).compact.first(limit)
names << "#{awards.size - names.size} more." if awards.size > names.size
diff --git a/app/helpers/labels_helper.rb b/app/helpers/labels_helper.rb
index 5e9f5837101..b9f3d6c75c2 100644
--- a/app/helpers/labels_helper.rb
+++ b/app/helpers/labels_helper.rb
@@ -115,8 +115,9 @@ module LabelsHelper
end
def labels_filter_path
- if @project
- namespace_project_labels_path(@project.namespace, @project, :json)
+ project = @target_project || @project
+ if project
+ namespace_project_labels_path(project.namespace, project, :json)
else
dashboard_labels_path(:json)
end
diff --git a/app/helpers/lfs_helper.rb b/app/helpers/lfs_helper.rb
index c15ecc8f86e..95b60aeab5f 100644
--- a/app/helpers/lfs_helper.rb
+++ b/app/helpers/lfs_helper.rb
@@ -1,11 +1,13 @@
module LfsHelper
+ include Gitlab::Routing.url_helpers
+
def require_lfs_enabled!
return if Gitlab.config.lfs.enabled
render(
json: {
message: 'Git LFS is not enabled on this GitLab server, contact your admin.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
status: 501
)
@@ -46,7 +48,7 @@ module LfsHelper
render(
json: {
message: 'Access forbidden. Check your access level.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 403
@@ -57,7 +59,7 @@ module LfsHelper
render(
json: {
message: 'Not found.',
- documentation_url: "#{Gitlab.config.gitlab.url}/help",
+ documentation_url: help_url,
},
content_type: "application/vnd.git-lfs+json",
status: 404
diff --git a/app/helpers/milestones_helper.rb b/app/helpers/milestones_helper.rb
index a11c313a6b8..83a2a4ad3ec 100644
--- a/app/helpers/milestones_helper.rb
+++ b/app/helpers/milestones_helper.rb
@@ -71,8 +71,9 @@ module MilestonesHelper
end
def milestones_filter_dropdown_path
- if @project
- namespace_project_milestones_path(@project.namespace, @project, :json)
+ project = @target_project || @project
+ if project
+ namespace_project_milestones_path(project.namespace, project, :json)
else
dashboard_milestones_path(:json)
end
diff --git a/app/helpers/page_layout_helper.rb b/app/helpers/page_layout_helper.rb
index 22387d66451..7d4d049101a 100644
--- a/app/helpers/page_layout_helper.rb
+++ b/app/helpers/page_layout_helper.rb
@@ -92,12 +92,8 @@ module PageLayoutHelper
end
end
- def fluid_layout(enabled = false)
- if @fluid_layout.nil?
- @fluid_layout = (current_user && current_user.layout == "fluid") || enabled
- else
- @fluid_layout
- end
+ def fluid_layout
+ current_user && current_user.layout == "fluid"
end
def blank_container(enabled = false)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 56477733ea2..e667c9e4e2e 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -139,7 +139,7 @@ module ProjectsHelper
end
options = options_for_select(options, selected: highest_available_option || @project.project_feature.public_send(field))
- content_tag(:select, options, name: "project[project_feature_attributes][#{field.to_s}]", id: "project_project_feature_attributes_#{field.to_s}", class: "pull-right form-control", data: { field: field }).html_safe
+ content_tag(:select, options, name: "project[project_feature_attributes][#{field}]", id: "project_project_feature_attributes_#{field}", class: "pull-right form-control", data: { field: field }).html_safe
end
private
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index 8a7446b7cc7..aba3a3f9c5d 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -153,8 +153,18 @@ module SearchHelper
search_path(options)
end
- # Sanitize html generated after parsing markdown from issue description or comment
- def search_md_sanitize(html)
+ # Sanitize a HTML field for search display. Most tags are stripped out and the
+ # maximum length is set to 200 characters.
+ def search_md_sanitize(object, field)
+ html = markdown_field(object, field)
+ html = Truncato.truncate(
+ html,
+ count_tags: false,
+ count_tail: false,
+ max_length: 200
+ )
+
+ # Truncato's filtered_tags and filtered_attributes are not quite the same
sanitize(html, tags: %w(a p ol ul li pre code))
end
end
diff --git a/app/helpers/selects_helper.rb b/app/helpers/selects_helper.rb
index 5f27e33c6ad..8706876ae4a 100644
--- a/app/helpers/selects_helper.rb
+++ b/app/helpers/selects_helper.rb
@@ -49,12 +49,10 @@ module SelectsHelper
end
def select2_tag(id, opts = {})
- css_class = ''
- css_class << 'multiselect ' if opts[:multiple]
- css_class << (opts[:class] || '')
+ opts[:class] << ' multiselect' if opts[:multiple]
value = opts[:selected] || ''
- hidden_field_tag(id, value, class: css_class)
+ hidden_field_tag(id, value, opts)
end
private
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index 1e86f648203..a9db8bb2b82 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -114,6 +114,26 @@ module TodosHelper
selected_type ? selected_type[:text] : default_type
end
+ def todo_due_date(todo)
+ return unless todo.target.try(:due_date)
+
+ is_due_today = todo.target.due_date.today?
+ is_overdue = todo.target.overdue?
+ css_class =
+ if is_due_today
+ 'text-warning'
+ elsif is_overdue
+ 'text-danger'
+ else
+ ''
+ end
+
+ html = "&middot; ".html_safe
+ html << content_tag(:span, class: css_class) do
+ "Due #{is_due_today ? "today" : todo.target.due_date.to_s(:medium)}"
+ end
+ end
+
private
def show_todo_state?(todo)
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index 415f6e12885..f7ed61625f4 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -3,4 +3,12 @@ class DeviseMailer < Devise::Mailer
default reply_to: Gitlab.config.gitlab.email_reply_to
layout 'devise_mailer'
+
+ protected
+
+ def subject_for(key)
+ subject = super
+ subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
+ subject
+ end
end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 45311690293..7b617b359ea 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -45,7 +45,7 @@ module Emails
@token = token
mail(to: member.invite_email,
- subject: "Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}")
+ subject: subject("Invitation to join the #{member_source.human_name} #{member_source.model_name.singular}"))
end
def member_invite_accepted_email(member_source_type, member_id)
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 9799f1dc886..2444702104e 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -92,6 +92,7 @@ class Notify < BaseMailer
subject = ""
subject << "#{@project.name} | " if @project
subject << extra.join(' | ') if extra.present?
+ subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
subject
end
@@ -109,7 +110,7 @@ class Notify < BaseMailer
headers['X-GitLab-Reply-Key'] = reply_key
if !@labels_url && @sent_notification && @sent_notification.unsubscribable?
- headers['List-Unsubscribe'] = unsubscribe_sent_notification_url(@sent_notification, force: true)
+ headers['List-Unsubscribe'] = "<#{unsubscribe_sent_notification_url(@sent_notification, force: true)}>"
@sent_notification_url = unsubscribe_sent_notification_url(@sent_notification)
end
diff --git a/app/models/abuse_report.rb b/app/models/abuse_report.rb
index b01a244032d..2340453831e 100644
--- a/app/models/abuse_report.rb
+++ b/app/models/abuse_report.rb
@@ -1,4 +1,8 @@
class AbuseReport < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :message, pipeline: :single_line
+
belongs_to :reporter, class_name: 'User'
belongs_to :user
@@ -7,6 +11,9 @@ class AbuseReport < ActiveRecord::Base
validates :message, presence: true
validates :user_id, uniqueness: { message: 'has already been reported' }
+ # For CacheMarkdownField
+ alias_method :author, :reporter
+
def remove_user(deleted_by:)
user.block
DeleteUserWorker.perform_async(deleted_by.id, user.id, delete_solo_owned_groups: true)
diff --git a/app/models/appearance.rb b/app/models/appearance.rb
index 4cf8dd9a8ce..e4106e1c2e9 100644
--- a/app/models/appearance.rb
+++ b/app/models/appearance.rb
@@ -1,4 +1,8 @@
class Appearance < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :description
+
validates :title, presence: true
validates :description, presence: true
validates :logo, file_size: { maximum: 1.megabyte }
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 55d2e07de08..c99aa7772bb 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -1,5 +1,7 @@
class ApplicationSetting < ActiveRecord::Base
+ include CacheMarkdownField
include TokenAuthenticatable
+
add_authentication_token_field :runners_registration_token
add_authentication_token_field :health_check_access_token
@@ -17,6 +19,11 @@ class ApplicationSetting < ActiveRecord::Base
serialize :domain_whitelist, Array
serialize :domain_blacklist, Array
+ cache_markdown_field :sign_in_text
+ cache_markdown_field :help_page_text
+ cache_markdown_field :shared_runners_text, pipeline: :plain_markdown
+ cache_markdown_field :after_sign_up_text
+
attr_accessor :domain_whitelist_raw, :domain_blacklist_raw
validates :session_expire_delay,
diff --git a/app/models/board.rb b/app/models/board.rb
index 3240c4bede3..c56422914a9 100644
--- a/app/models/board.rb
+++ b/app/models/board.rb
@@ -4,4 +4,12 @@ class Board < ActiveRecord::Base
has_many :lists, -> { order(:list_type, :position) }, dependent: :delete_all
validates :project, presence: true
+
+ def backlog_list
+ lists.merge(List.backlog).take
+ end
+
+ def done_list
+ lists.merge(List.done).take
+ end
end
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index 61498140f27..cb40f33932a 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -1,6 +1,9 @@
class BroadcastMessage < ActiveRecord::Base
+ include CacheMarkdownField
include Sortable
+ cache_markdown_field :message, pipeline: :broadcast_message
+
validates :message, presence: true
validates :starts_at, presence: true
validates :ends_at, presence: true
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 522e2264bb8..5dbf66173de 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -373,7 +373,7 @@ module Ci
end
def artifacts?
- !artifacts_expired? && self[:artifacts_file].present?
+ !artifacts_expired? && artifacts_file.exists?
end
def artifacts_metadata?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 663c5b1e231..2cf9892edc5 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -196,7 +196,7 @@ module Ci
end
def has_warnings?
- builds.latest.ignored.any?
+ builds.latest.failed_but_allowed.any?
end
def config_processor
@@ -251,9 +251,8 @@ module Ci
Ci::ProcessPipelineService.new(project, user).execute(self)
end
- def build_updated
+ def update_status
with_lock do
- reload
case latest_builds_status
when 'pending' then enqueue
when 'running' then run
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index ed5d4b13b7e..44cb19ece3b 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -2,7 +2,7 @@ module Ci
class Runner < ActiveRecord::Base
extend Ci::Model
- LAST_CONTACT_TIME = 2.hours.ago
+ LAST_CONTACT_TIME = 1.hour.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
FORM_EDITABLE = %i[description tag_list active run_untagged locked]
diff --git a/app/models/commit_range.rb b/app/models/commit_range.rb
index 656a242c265..ac2477fd973 100644
--- a/app/models/commit_range.rb
+++ b/app/models/commit_range.rb
@@ -80,7 +80,7 @@ class CommitRange
end
def inspect
- %(#<#{self.class}:#{object_id} #{to_s}>)
+ %(#<#{self.class}:#{object_id} #{self}>)
end
def to_s
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 736db1ab0f6..5d6d534cd31 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -1,6 +1,7 @@
class CommitStatus < ActiveRecord::Base
include HasStatus
include Importable
+ include AfterCommitQueue
self.table_name = 'ci_builds'
@@ -24,7 +25,22 @@ class CommitStatus < ActiveRecord::Base
scope :retried, -> { where.not(id: latest) }
scope :ordered, -> { order(:name) }
- scope :ignored, -> { where(allow_failure: true, status: [:failed, :canceled]) }
+
+ scope :failed_but_allowed, -> do
+ where(allow_failure: true, status: [:failed, :canceled])
+ end
+
+ scope :exclude_ignored, -> do
+ quoted_when = connection.quote_column_name('when')
+ # We want to ignore failed_but_allowed jobs
+ where("allow_failure = ? OR status IN (?)",
+ false, all_state_names - [:failed, :canceled]).
+ # We want to ignore skipped manual jobs
+ where("#{quoted_when} <> ? OR status <> ?", 'manual', 'skipped').
+ # We want to ignore skipped on_failure
+ where("#{quoted_when} <> ? OR status <> ?", 'on_failure', 'skipped')
+ end
+
scope :latest_ci_stages, -> { latest.ordered.includes(project: :namespace) }
scope :retried_ci_stages, -> { retried.ordered.includes(project: :namespace) }
@@ -69,21 +85,35 @@ class CommitStatus < ActiveRecord::Base
commit_status.update_attributes finished_at: Time.now
end
- after_transition any => [:success, :failed, :canceled] do |commit_status|
- commit_status.pipeline.try(:process!)
- true
- end
-
after_transition do |commit_status, transition|
- commit_status.pipeline.try(:build_updated) unless transition.loopback?
+ return if transition.loopback?
+
+ commit_status.run_after_commit do
+ pipeline.try do |pipeline|
+ if complete?
+ ProcessPipelineWorker.perform_async(pipeline.id)
+ else
+ UpdatePipelineWorker.perform_async(pipeline.id)
+ end
+ end
+ end
end
after_transition [:created, :pending, :running] => :success do |commit_status|
- MergeRequests::MergeWhenBuildSucceedsService.new(commit_status.pipeline.project, nil).trigger(commit_status)
+ commit_status.run_after_commit do
+ # TODO, temporary fix for race condition
+ UpdatePipelineWorker.new.perform(pipeline.id)
+
+ MergeRequests::MergeWhenBuildSucceedsService
+ .new(pipeline.project, nil).trigger(self)
+ end
end
after_transition any => :failed do |commit_status|
- MergeRequests::AddTodoWhenBuildFailsService.new(commit_status.pipeline.project, nil).execute(commit_status)
+ commit_status.run_after_commit do
+ MergeRequests::AddTodoWhenBuildFailsService
+ .new(pipeline.project, nil).execute(self)
+ end
end
end
@@ -111,7 +141,7 @@ class CommitStatus < ActiveRecord::Base
end
end
- def ignored?
+ def failed_but_allowed?
allow_failure? && (failed? || canceled?)
end
diff --git a/app/models/concerns/access_requestable.rb b/app/models/concerns/access_requestable.rb
index eedd32a729f..62bc6b809f4 100644
--- a/app/models/concerns/access_requestable.rb
+++ b/app/models/concerns/access_requestable.rb
@@ -8,9 +8,6 @@ module AccessRequestable
extend ActiveSupport::Concern
def request_access(user)
- members.create(
- access_level: Gitlab::Access::DEVELOPER,
- user: user,
- requested_at: Time.now.utc)
+ Members::RequestAccessService.new(self, user).execute
end
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
new file mode 100644
index 00000000000..90bd6490a02
--- /dev/null
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -0,0 +1,131 @@
+# This module takes care of updating cache columns for Markdown-containing
+# fields. Use like this in the body of your class:
+#
+# include CacheMarkdownField
+# cache_markdown_field :foo
+# cache_markdown_field :bar
+# cache_markdown_field :baz, pipeline: :single_line
+#
+# Corresponding foo_html, bar_html and baz_html fields should exist.
+module CacheMarkdownField
+ # Knows about the relationship between markdown and html field names, and
+ # stores the rendering contexts for the latter
+ class FieldData
+ extend Forwardable
+
+ def initialize
+ @data = {}
+ end
+
+ def_delegators :@data, :[], :[]=
+ def_delegator :@data, :keys, :markdown_fields
+
+ def html_field(markdown_field)
+ "#{markdown_field}_html"
+ end
+
+ def html_fields
+ markdown_fields.map {|field| html_field(field) }
+ end
+ end
+
+ # Dynamic registries don't really work in Rails as it's not guaranteed that
+ # every class will be loaded, so hardcode the list.
+ CACHING_CLASSES = %w[
+ AbuseReport
+ Appearance
+ ApplicationSetting
+ BroadcastMessage
+ Issue
+ Label
+ MergeRequest
+ Milestone
+ Namespace
+ Note
+ Project
+ Release
+ Snippet
+ ]
+
+ def self.caching_classes
+ CACHING_CLASSES.map(&:constantize)
+ end
+
+ extend ActiveSupport::Concern
+
+ included do
+ cattr_reader :cached_markdown_fields do
+ FieldData.new
+ end
+
+ # Returns the default Banzai render context for the cached markdown field.
+ def banzai_render_context(field)
+ raise ArgumentError.new("Unknown field: #{field.inspect}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
+
+ # Always include a project key, or Banzai complains
+ project = self.project if self.respond_to?(:project)
+ context = cached_markdown_fields[field].merge(project: project)
+
+ # Banzai is less strict about authors, so don't always have an author key
+ context[:author] = self.author if self.respond_to?(:author)
+
+ context
+ end
+
+ # Allow callers to look up the cache field name, rather than hardcoding it
+ def markdown_cache_field_for(field)
+ raise ArgumentError.new("Unknown field: #{field}") unless
+ cached_markdown_fields.markdown_fields.include?(field)
+
+ cached_markdown_fields.html_field(field)
+ end
+
+ # Always exclude _html fields from attributes (including serialization).
+ # They contain unredacted HTML, which would be a security issue
+ alias_method :attributes_before_markdown_cache, :attributes
+ def attributes
+ attrs = attributes_before_markdown_cache
+
+ cached_markdown_fields.html_fields.each do |field|
+ attrs.delete(field)
+ end
+
+ attrs
+ end
+ end
+
+ class_methods do
+ private
+
+ # Specify that a field is markdown. Its rendered output will be cached in
+ # a corresponding _html field. Any custom rendering options may be provided
+ # as a context.
+ def cache_markdown_field(markdown_field, context = {})
+ raise "Add #{self} to CacheMarkdownField::CACHING_CLASSES" unless
+ CacheMarkdownField::CACHING_CLASSES.include?(self.to_s)
+
+ cached_markdown_fields[markdown_field] = context
+
+ html_field = cached_markdown_fields.html_field(markdown_field)
+ cache_method = "#{markdown_field}_cache_refresh".to_sym
+ invalidation_method = "#{html_field}_invalidated?".to_sym
+
+ define_method(cache_method) do
+ html = Banzai::Renderer.cacheless_render_field(self, markdown_field)
+ __send__("#{html_field}=", html)
+ true
+ end
+
+ # The HTML becomes invalid if any dependent fields change. For now, assume
+ # author and project invalidate the cache in all circumstances.
+ define_method(invalidation_method) do
+ changed_fields = changed_attributes.keys
+ invalidations = changed_fields & [markdown_field.to_s, "author", "project"]
+ !invalidations.empty?
+ end
+
+ before_save cache_method, if: invalidation_method
+ end
+ end
+end
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 0fa4df0fb56..9f64f76721d 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -8,32 +8,32 @@ module HasStatus
class_methods do
def status_sql
- scope = all
+ scope = if respond_to?(:exclude_ignored)
+ exclude_ignored
+ else
+ all
+ end
builds = scope.select('count(*)').to_sql
created = scope.created.select('count(*)').to_sql
success = scope.success.select('count(*)').to_sql
- ignored = scope.ignored.select('count(*)').to_sql if scope.respond_to?(:ignored)
- ignored ||= '0'
pending = scope.pending.select('count(*)').to_sql
running = scope.running.select('count(*)').to_sql
- canceled = scope.canceled.select('count(*)').to_sql
skipped = scope.skipped.select('count(*)').to_sql
+ canceled = scope.canceled.select('count(*)').to_sql
- deduce_status = "(CASE
+ "(CASE
+ WHEN (#{builds})=(#{success}) THEN 'success'
WHEN (#{builds})=(#{created}) THEN 'created'
- WHEN (#{builds})=(#{skipped}) THEN 'skipped'
- WHEN (#{builds})=(#{success})+(#{ignored})+(#{skipped}) THEN 'success'
- WHEN (#{builds})=(#{created})+(#{pending})+(#{skipped}) THEN 'pending'
- WHEN (#{builds})=(#{canceled})+(#{success})+(#{ignored})+(#{skipped}) THEN 'canceled'
+ WHEN (#{builds})=(#{success})+(#{skipped}) THEN 'skipped'
+ WHEN (#{builds})=(#{success})+(#{skipped})+(#{canceled}) THEN 'canceled'
+ WHEN (#{builds})=(#{created})+(#{skipped})+(#{pending}) THEN 'pending'
WHEN (#{running})+(#{pending})+(#{created})>0 THEN 'running'
ELSE 'failed'
END)"
-
- deduce_status
end
def status
- all.pluck(self.status_sql).first
+ all.pluck(status_sql).first
end
def started_at
@@ -43,6 +43,10 @@ module HasStatus
def finished_at
all.maximum(:finished_at)
end
+
+ def all_state_names
+ state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
+ end
end
included do
diff --git a/app/models/concerns/issuable.rb b/app/models/concerns/issuable.rb
index ff465d2c745..c4b42ad82c7 100644
--- a/app/models/concerns/issuable.rb
+++ b/app/models/concerns/issuable.rb
@@ -6,6 +6,7 @@
#
module Issuable
extend ActiveSupport::Concern
+ include CacheMarkdownField
include Participable
include Mentionable
include Subscribable
@@ -13,6 +14,9 @@ module Issuable
include Awardable
included do
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
belongs_to :author, class_name: "User"
belongs_to :assignee, class_name: "User"
belongs_to :updated_by, class_name: "User"
diff --git a/app/models/concerns/mentionable.rb b/app/models/concerns/mentionable.rb
index ec9e0f1b1d0..eb2ff0428f6 100644
--- a/app/models/concerns/mentionable.rb
+++ b/app/models/concerns/mentionable.rb
@@ -43,19 +43,15 @@ module Mentionable
self
end
- def all_references(current_user = nil, text = nil, extractor: nil)
+ def all_references(current_user = nil, extractor: nil)
extractor ||= Gitlab::ReferenceExtractor.
new(project, current_user)
- if text
- extractor.analyze(text, author: author)
- else
- self.class.mentionable_attrs.each do |attr, options|
- text = __send__(attr)
- options = options.merge(cache_key: [self, attr], author: author)
+ self.class.mentionable_attrs.each do |attr, options|
+ text = __send__(attr)
+ options = options.merge(cache_key: [self, attr], author: author)
- extractor.analyze(text, options)
- end
+ extractor.analyze(text, options)
end
extractor
@@ -66,8 +62,8 @@ module Mentionable
end
# Extract GFM references to other Mentionables from this Mentionable. Always excludes its #local_reference.
- def referenced_mentionables(current_user = self.author, text = nil)
- refs = all_references(current_user, text)
+ def referenced_mentionables(current_user = self.author)
+ refs = all_references(current_user)
refs = (refs.issues + refs.merge_requests + refs.commits)
# We're using this method instead of Array diffing because that requires
@@ -77,8 +73,8 @@ module Mentionable
end
# Create a cross-reference Note for each GFM reference to another Mentionable found in the +mentionable_attrs+.
- def create_cross_references!(author = self.author, without = [], text = nil)
- refs = referenced_mentionables(author, text)
+ def create_cross_references!(author = self.author, without = [])
+ refs = referenced_mentionables(author)
# We're using this method instead of Array diffing because that requires
# both of the object's `hash` values to be the same, which may not be the
@@ -97,10 +93,7 @@ module Mentionable
return if changes.empty?
- original_text = changes.collect { |_, vals| vals.first }.join(' ')
-
- preexisting = referenced_mentionables(author, original_text)
- create_cross_references!(author, preexisting)
+ create_cross_references!(author)
end
private
diff --git a/app/models/cycle_analytics/summary.rb b/app/models/cycle_analytics/summary.rb
index 53b2cacb131..b46db449bf3 100644
--- a/app/models/cycle_analytics/summary.rb
+++ b/app/models/cycle_analytics/summary.rb
@@ -10,15 +10,33 @@ class CycleAnalytics
end
def commits
- repository = @project.repository.raw_repository
-
- if @project.default_branch
- repository.log(ref: @project.default_branch, after: @from).count
- end
+ ref = @project.default_branch.presence
+ count_commits_for(ref)
end
def deploys
@project.deployments.where("created_at > ?", @from).count
end
+
+ private
+
+ # Don't use the `Gitlab::Git::Repository#log` method, because it enforces
+ # a limit. Since we need a commit count, we _can't_ enforce a limit, so
+ # the easiest way forward is to replicate the relevant portions of the
+ # `log` function here.
+ def count_commits_for(ref)
+ return unless ref
+
+ repository = @project.repository.raw_repository
+ sha = @project.repository.commit(ref).sha
+
+ cmd = %W(git --git-dir=#{repository.path} log)
+ cmd << '--format=%H'
+ cmd << "--after=#{@from.iso8601}"
+ cmd << sha
+
+ raw_output = IO.popen(cmd) { |io| io.read }
+ raw_output.lines.count
+ end
end
end
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 07d7e19e70d..82b27b78229 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -11,7 +11,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
- after_save :keep_around_commit
+ after_save :create_ref
def commit
project.commit(sha)
@@ -29,8 +29,8 @@ class Deployment < ActiveRecord::Base
self == environment.last_deployment
end
- def keep_around_commit
- project.repository.keep_around(self.sha)
+ def create_ref
+ project.repository.create_ref(ref, ref_path)
end
def manual_actions
@@ -76,4 +76,10 @@ class Deployment < ActiveRecord::Base
where.not(id: self.id).
take
end
+
+ private
+
+ def ref_path
+ File.join(environment.ref_path, 'deployments', id.to_s)
+ end
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 49e0a20640c..f0f3ee23223 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -47,4 +47,8 @@ class Environment < ActiveRecord::Base
def update_merge_request_metrics?
self.name == "production"
end
+
+ def ref_path
+ "refs/environments/#{Shellwords.shellescape(name)}"
+ end
end
diff --git a/app/models/event.rb b/app/models/event.rb
index 55a76e26f3c..314d5ba438f 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -328,13 +328,15 @@ class Event < ActiveRecord::Base
def reset_project_activity
return unless project
- # Don't even bother obtaining a lock if the last update happened less than
- # 60 minutes ago.
+ # Don't bother updating if we know the project was updated recently.
return if recent_update?
- return unless try_obtain_lease
-
- project.update_column(:last_activity_at, created_at)
+ # At this point it's possible for multiple threads/processes to try to
+ # update the project. Only one query should actually perform the update,
+ # hence we add the extra WHERE clause for last_activity_at.
+ Project.unscoped.where(id: project_id).
+ where('last_activity_at <= ?', RESET_PROJECT_ACTIVITY_INTERVAL.ago).
+ update_all(last_activity_at: created_at)
end
private
@@ -342,11 +344,4 @@ class Event < ActiveRecord::Base
def recent_update?
project.last_activity_at > RESET_PROJECT_ACTIVITY_INTERVAL.ago
end
-
- def try_obtain_lease
- Gitlab::ExclusiveLease.
- new("project:update_last_activity_at:#{project.id}",
- timeout: RESET_PROJECT_ACTIVITY_INTERVAL.to_i).
- try_obtain
- end
end
diff --git a/app/models/global_label.rb b/app/models/global_label.rb
index ddd4bad5c21..698a7bbd327 100644
--- a/app/models/global_label.rb
+++ b/app/models/global_label.rb
@@ -4,6 +4,10 @@ class GlobalLabel
delegate :color, :description, to: :@first_label
+ def for_display
+ @first_label
+ end
+
def self.build_collection(labels)
labels = labels.group_by(&:title)
diff --git a/app/models/global_milestone.rb b/app/models/global_milestone.rb
index bda2b5c5d5d..cde4a568577 100644
--- a/app/models/global_milestone.rb
+++ b/app/models/global_milestone.rb
@@ -4,6 +4,10 @@ class GlobalMilestone
attr_accessor :title, :milestones
alias_attribute :name, :title
+ def for_display
+ @first_milestone
+ end
+
def self.build_collection(milestones)
milestones = milestones.group_by(&:title)
@@ -17,6 +21,7 @@ class GlobalMilestone
@title = title
@name = title
@milestones = milestones
+ @first_milestone = milestones.find {|m| m.description.present? } || milestones.first
end
def safe_title
diff --git a/app/models/group.rb b/app/models/group.rb
index aefb94b2ada..a2f88cca828 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -102,40 +102,44 @@ class Group < Namespace
self[:lfs_enabled]
end
- def add_users(user_ids, access_level, current_user: nil, expires_at: nil)
- user_ids.each do |user_id|
- Member.add_user(
- self.group_members,
- user_id,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
+ def add_users(users, access_level, current_user: nil, expires_at: nil)
+ GroupMember.add_users_to_group(
+ self,
+ users,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
def add_user(user, access_level, current_user: nil, expires_at: nil)
- add_users([user], access_level, current_user: current_user, expires_at: expires_at)
+ GroupMember.add_user(
+ self,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
def add_guest(user, current_user = nil)
- add_user(user, Gitlab::Access::GUEST, current_user: current_user)
+ add_user(user, :guest, current_user: current_user)
end
def add_reporter(user, current_user = nil)
- add_user(user, Gitlab::Access::REPORTER, current_user: current_user)
+ add_user(user, :reporter, current_user: current_user)
end
def add_developer(user, current_user = nil)
- add_user(user, Gitlab::Access::DEVELOPER, current_user: current_user)
+ add_user(user, :developer, current_user: current_user)
end
def add_master(user, current_user = nil)
- add_user(user, Gitlab::Access::MASTER, current_user: current_user)
+ add_user(user, :master, current_user: current_user)
end
def add_owner(user, current_user = nil)
- add_user(user, Gitlab::Access::OWNER, current_user: current_user)
+ add_user(user, :owner, current_user: current_user)
end
def has_owner?(user)
diff --git a/app/models/label.rb b/app/models/label.rb
index a23140b7d64..e8e12e2904e 100644
--- a/app/models/label.rb
+++ b/app/models/label.rb
@@ -1,4 +1,5 @@
class Label < ActiveRecord::Base
+ include CacheMarkdownField
include Referable
include Subscribable
@@ -8,6 +9,8 @@ class Label < ActiveRecord::Base
None = LabelStruct.new('No Label', 'No Label')
Any = LabelStruct.new('Any Label', '')
+ cache_markdown_field :description, pipeline: :single_line
+
DEFAULT_COLOR = '#428BCA'
default_value_for :color, DEFAULT_COLOR
diff --git a/app/models/member.rb b/app/models/member.rb
index 69406379948..b89ba8ecbb8 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -80,49 +80,75 @@ class Member < ActiveRecord::Base
find_by(invite_token: invite_token)
end
- # This method is used to find users that have been entered into the "Add members" field.
- # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
- def user_for_id(user_id)
- return user_id if user_id.is_a?(User)
-
- user = User.find_by(id: user_id)
- user ||= User.find_by(email: user_id)
- user ||= user_id
- user
- end
-
- def add_user(members, user_id, access_level, current_user: nil, expires_at: nil)
- user = user_for_id(user_id)
+ def add_user(source, user, access_level, current_user: nil, expires_at: nil)
+ user = retrieve_user(user)
+ access_level = retrieve_access_level(access_level)
# `user` can be either a User object or an email to be invited
- if user.is_a?(User)
- member = members.find_or_initialize_by(user_id: user.id)
+ member =
+ if user.is_a?(User)
+ source.members.find_by(user_id: user.id) ||
+ source.requesters.find_by(user_id: user.id) ||
+ source.members.build(user_id: user.id)
+ else
+ source.members.build(invite_email: user)
+ end
+
+ return member unless can_update_member?(current_user, member)
+
+ member.attributes = {
+ created_by: member.created_by || current_user,
+ access_level: access_level,
+ expires_at: expires_at
+ }
+
+ if member.request?
+ ::Members::ApproveAccessRequestService.new(
+ source,
+ current_user,
+ id: member.id,
+ access_level: access_level
+ ).execute
else
- member = members.build
- member.invite_email = user
+ member.save
end
- if can_update_member?(current_user, member) || project_creator?(member, access_level)
- member.created_by ||= current_user
- member.access_level = access_level
- member.expires_at = expires_at
+ member
+ end
- member.save
- end
+ def access_levels
+ Gitlab::Access.sym_options
end
private
+ # This method is used to find users that have been entered into the "Add members" field.
+ # These can be the User objects directly, their IDs, their emails, or new emails to be invited.
+ def retrieve_user(user)
+ return user if user.is_a?(User)
+
+ User.find_by(id: user) || User.find_by(email: user) || user
+ end
+
+ def retrieve_access_level(access_level)
+ access_levels.fetch(access_level) { access_level.to_i }
+ end
+
def can_update_member?(current_user, member)
# There is no current user for bulk actions, in which case anything is allowed
- !current_user ||
- current_user.can?(:update_group_member, member) ||
- current_user.can?(:update_project_member, member)
+ !current_user || current_user.can?(:"update_#{member.type.underscore}", member)
end
- def project_creator?(member, access_level)
- member.new_record? && member.owner? &&
- access_level.to_i == ProjectMember::MASTER
+ def add_users_to_source(source, users, access_level, current_user: nil, expires_at: nil)
+ users.each do |user|
+ add_user(
+ source,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
+ end
end
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 2f13d339c89..1b54a85d064 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,6 +12,22 @@ class GroupMember < Member
Gitlab::Access.options_with_owner
end
+ def self.access_levels
+ Gitlab::Access.sym_options_with_owner
+ end
+
+ def self.add_users_to_group(group, users, access_level, current_user: nil, expires_at: nil)
+ self.transaction do
+ add_users_to_source(
+ group,
+ users,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
+ end
+ end
+
def group
source
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index ec2d40eb11c..125f26369d7 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -34,36 +34,20 @@ class ProjectMember < Member
# :master
# )
#
- def add_users_to_projects(project_ids, user_ids, access, current_user: nil, expires_at: nil)
- access_level = if roles_hash.has_key?(access)
- roles_hash[access]
- elsif roles_hash.values.include?(access.to_i)
- access
- else
- raise "Non valid access"
- end
-
- users = user_ids.map { |user_id| Member.user_for_id(user_id) }
-
- ProjectMember.transaction do
+ def add_users_to_projects(project_ids, users, access_level, current_user: nil, expires_at: nil)
+ self.transaction do
project_ids.each do |project_id|
project = Project.find(project_id)
- users.each do |user|
- Member.add_user(
- project.project_members,
- user,
- access_level,
- current_user: current_user,
- expires_at: expires_at
- )
- end
+ add_users_to_source(
+ project,
+ users,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
end
-
- true
- rescue
- false
end
def truncate_teams(project_ids)
@@ -84,13 +68,15 @@ class ProjectMember < Member
truncate_teams [project.id]
end
- def roles_hash
- Gitlab::Access.sym_options
- end
-
def access_level_roles
Gitlab::Access.options
end
+
+ private
+
+ def can_update_member?(current_user, member)
+ super || (member.owner? && member.new_record?)
+ end
end
def access_field
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 2dcf7f89bfc..a743bf313ae 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -31,7 +31,7 @@ class MergeRequest < ActiveRecord::Base
# Temporary fields to store compare vars
# when creating new merge request
- attr_accessor :can_be_created, :compare_commits, :compare
+ attr_accessor :can_be_created, :compare_commits, :diff_options, :compare
state_machine :state, initial: :opened do
event :close do
@@ -155,6 +155,20 @@ class MergeRequest < ActiveRecord::Base
where("merge_requests.id IN (#{union.to_sql})")
end
+ WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
+
+ def self.work_in_progress?(title)
+ !!(title =~ WIP_REGEX)
+ end
+
+ def self.wipless_title(title)
+ title.sub(WIP_REGEX, "")
+ end
+
+ def self.wip_title(title)
+ work_in_progress?(title) ? title : "WIP: #{title}"
+ end
+
def to_reference(from_project = nil)
reference = "#{self.class.reference_prefix}#{iid}"
@@ -182,7 +196,7 @@ class MergeRequest < ActiveRecord::Base
end
def diff_size
- merge_request_diff.size
+ diffs(diff_options).size
end
def diff_base_commit
@@ -389,14 +403,16 @@ class MergeRequest < ActiveRecord::Base
@closed_event ||= target_project.events.where(target_id: self.id, target_type: "MergeRequest", action: Event::CLOSED).last
end
- WIP_REGEX = /\A\s*(\[WIP\]\s*|WIP:\s*|WIP\s+)+\s*/i.freeze
-
def work_in_progress?
- !!(title =~ WIP_REGEX)
+ self.class.work_in_progress?(title)
end
def wipless_title
- self.title.sub(WIP_REGEX, "")
+ self.class.wipless_title(self.title)
+ end
+
+ def wip_title
+ self.class.wip_title(self.title)
end
def mergeable?(skip_ci_check: false)
@@ -507,9 +523,13 @@ class MergeRequest < ActiveRecord::Base
# `MergeRequestsClosingIssues` model. This is a performance optimization.
# Calculating this information for a number of merge requests requires
# running `ReferenceExtractor` on each of them separately.
+ # This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user = self.author)
+ return if project.has_external_issue_tracker?
+
transaction do
self.merge_requests_closing_issues.delete_all
+
closes_issues(current_user).each do |issue|
self.merge_requests_closing_issues.create!(issue: issue)
end
@@ -590,13 +610,11 @@ class MergeRequest < ActiveRecord::Base
end
def merge_commit_message
- message = "Merge branch '#{source_branch}' into '#{target_branch}'"
- message << "\n\n"
- message << title.to_s
- message << "\n\n"
- message << description.to_s
- message << "\n\n"
- message << "See merge request !#{iid}"
+ message = "Merge branch '#{source_branch}' into '#{target_branch}'\n\n"
+ message << "#{title}\n\n"
+ message << "#{description}\n\n" if description.present?
+ message << "See merge request #{to_reference}"
+
message
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 36b8b70870b..3f7e96186a1 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -6,6 +6,9 @@ class MergeRequestDiff < ActiveRecord::Base
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
+ # Valid types of serialized diffs allowed by Gitlab::Git::Diff
+ VALID_CLASSES = [Hash, Rugged::Patch, Rugged::Diff::Delta]
+
belongs_to :merge_request
state_machine :state, initial: :empty do
@@ -170,6 +173,15 @@ class MergeRequestDiff < ActiveRecord::Base
private
+ # Old GitLab implementations may have generated diffs as ["--broken-diff"].
+ # Avoid an error 500 by ignoring bad elements. See:
+ # https://gitlab.com/gitlab-org/gitlab-ce/issues/20776
+ def valid_raw_diff?(raw)
+ return false unless raw.respond_to?(:each)
+
+ raw.any? { |element| VALID_CLASSES.include?(element.class) }
+ end
+
def dump_commits(commits)
commits.map(&:to_hash)
end
@@ -200,7 +212,7 @@ class MergeRequestDiff < ActiveRecord::Base
end
def load_diffs(raw, options)
- if raw.respond_to?(:each)
+ if valid_raw_diff?(raw)
if paths = options[:paths]
raw = raw.select do |diff|
paths.include?(diff[:old_path]) || paths.include?(diff[:new_path])
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index 2bd7f198030..23aecbfa3a6 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -6,12 +6,16 @@ class Milestone < ActiveRecord::Base
Any = MilestoneStruct.new('Any Milestone', '', -1)
Upcoming = MilestoneStruct.new('Upcoming', '#upcoming', -2)
+ include CacheMarkdownField
include InternalId
include Sortable
include Referable
include StripAttribute
include Milestoneish
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :description
+
belongs_to :project
has_many :issues
has_many :labels, -> { distinct.reorder('labels.title') }, through: :issues
@@ -158,7 +162,7 @@ class Milestone < ActiveRecord::Base
end
def title=(value)
- write_attribute(:title, Sanitize.clean(value.to_s)) if value.present?
+ write_attribute(:title, sanitize_title(value)) if value.present?
end
# Sorts the issues for the given IDs.
@@ -204,4 +208,8 @@ class Milestone < ActiveRecord::Base
iid
end
end
+
+ def sanitize_title(value)
+ CGI.unescape_html(Sanitize.clean(value.to_s))
+ end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 919b3b1f095..b67049f0f55 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -1,9 +1,12 @@
class Namespace < ActiveRecord::Base
acts_as_paranoid
+ include CacheMarkdownField
include Sortable
include Gitlab::ShellAdapter
+ cache_markdown_field :description, pipeline: :description
+
has_many :projects, dependent: :destroy
belongs_to :owner, class_name: "User"
@@ -58,15 +61,13 @@ class Namespace < ActiveRecord::Base
def clean_path(path)
path = path.dup
# Get the email username by removing everything after an `@` sign.
- path.gsub!(/@.*\z/, "")
- # Usernames can't end in .git, so remove it.
- path.gsub!(/\.git\z/, "")
- # Remove dashes at the start of the username.
- path.gsub!(/\A-+/, "")
- # Remove periods at the end of the username.
- path.gsub!(/\.+\z/, "")
+ path.gsub!(/@.*\z/, "")
# Remove everything that's not in the list of allowed characters.
- path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+ path.gsub!(/[^a-zA-Z0-9_\-\.]/, "")
+ # Remove trailing violations ('.atom', '.git', or '.')
+ path.gsub!(/(\.atom|\.git|\.)*\z/, "")
+ # Remove leading violations ('-')
+ path.gsub!(/\A\-+/, "")
# Users with the great usernames of "." or ".." would end up with a blank username.
# Work around that by setting their username to "blank", followed by a counter.
diff --git a/app/models/note.rb b/app/models/note.rb
index f2656df028b..2d644b03e4d 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -6,10 +6,13 @@ class Note < ActiveRecord::Base
include Awardable
include Importable
include FasterCacheKeys
+ include CacheMarkdownField
+
+ cache_markdown_field :note, pipeline: :note
# Attribute containing rendered and redacted Markdown as generated by
# Banzai::ObjectRenderer.
- attr_accessor :note_html
+ attr_accessor :redacted_note_html
# An Array containing the number of visible references as generated by
# Banzai::ObjectRenderer
diff --git a/app/models/project.rb b/app/models/project.rb
index 7265cb55594..74d54e69648 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -6,6 +6,7 @@ class Project < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Gitlab::CurrentSettings
include AccessRequestable
+ include CacheMarkdownField
include Referable
include Sortable
include AfterCommitQueue
@@ -17,6 +18,8 @@ class Project < ActiveRecord::Base
UNKNOWN_IMPORT_URL = 'http://unknown.git'
+ cache_markdown_field :description, pipeline: :description
+
delegate :feature_available?, :builds_enabled?, :wiki_enabled?, :merge_requests_enabled?, to: :project_feature, allow_nil: true
default_value_for :archived, false
@@ -146,6 +149,7 @@ class Project < ActiveRecord::Base
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
+ delegate :add_user, to: :team
# Validations
validates :creator, presence: true, on: :create
@@ -371,18 +375,9 @@ class Project < ActiveRecord::Base
%r{(?<project>#{name_pattern}/#{name_pattern})}
end
- def trending(since = 1.month.ago)
- # By counting in the JOIN we don't expose the GROUP BY to the outer query.
- # This means that calls such as "any?" and "count" just return a number of
- # the total count, instead of the counts grouped per project as a Hash.
- join_body = "INNER JOIN (
- SELECT project_id, COUNT(*) AS amount
- FROM notes
- WHERE created_at >= #{sanitize(since)}
- GROUP BY project_id
- ) join_note_counts ON projects.id = join_note_counts.project_id"
-
- joins(join_body).reorder('join_note_counts.amount DESC')
+ def trending
+ joins('INNER JOIN trending_projects ON projects.id = trending_projects.project_id').
+ reorder('trending_projects.id ASC')
end
def cached_count
@@ -1016,10 +1011,6 @@ class Project < ActiveRecord::Base
project_members.find_by(user_id: user)
end
- def add_user(user, access_level, current_user: nil, expires_at: nil)
- team.add_user(user, access_level, current_user: current_user, expires_at: expires_at)
- end
-
def default_branch
@default_branch ||= repository.root_ref if repository.exists?
end
diff --git a/app/models/project_feature.rb b/app/models/project_feature.rb
index 8c9534c3565..530f7d5a30e 100644
--- a/app/models/project_feature.rb
+++ b/app/models/project_feature.rb
@@ -20,7 +20,10 @@ class ProjectFeature < ActiveRecord::Base
FEATURES = %i(issues merge_requests wiki snippets builds)
- belongs_to :project
+ # Default scopes force us to unscope here since a service may need to check
+ # permissions for a project in pending_delete
+ # http://stackoverflow.com/questions/1540645/how-to-disable-default-scope-for-a-belongs-to
+ belongs_to :project, -> { unscope(where: :pending_delete) }
default_value_for :builds_access_level, value: ENABLED, allows_nil: false
default_value_for :issues_access_level, value: ENABLED, allows_nil: false
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index 63a5ed14484..d9fba3d4a41 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -9,6 +9,10 @@ class CustomIssueTrackerService < IssueTrackerService
end
end
+ def title=(value)
+ self.properties['title'] = value if self.properties
+ end
+
def description
if self.properties && self.properties['description'].present?
self.properties['description']
diff --git a/app/models/project_services/slack_service/issue_message.rb b/app/models/project_services/slack_service/issue_message.rb
index 88e053ec192..cd87a79d0c6 100644
--- a/app/models/project_services/slack_service/issue_message.rb
+++ b/app/models/project_services/slack_service/issue_message.rb
@@ -11,7 +11,7 @@ class SlackService
attr_reader :description
def initialize(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_services/slack_service/merge_message.rb b/app/models/project_services/slack_service/merge_message.rb
index 11fc691022b..b7615c96068 100644
--- a/app/models/project_services/slack_service/merge_message.rb
+++ b/app/models/project_services/slack_service/merge_message.rb
@@ -10,7 +10,7 @@ class SlackService
attr_reader :title
def initialize(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_services/slack_service/note_message.rb b/app/models/project_services/slack_service/note_message.rb
index 89ba51cb662..9e84e90f38c 100644
--- a/app/models/project_services/slack_service/note_message.rb
+++ b/app/models/project_services/slack_service/note_message.rb
@@ -10,7 +10,7 @@ class SlackService
def initialize(params)
params = HashWithIndifferentAccess.new(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_services/slack_service/wiki_page_message.rb b/app/models/project_services/slack_service/wiki_page_message.rb
index f336d9e7691..160ca3ac115 100644
--- a/app/models/project_services/slack_service/wiki_page_message.rb
+++ b/app/models/project_services/slack_service/wiki_page_message.rb
@@ -9,7 +9,7 @@ class SlackService
attr_reader :description
def initialize(params)
- @user_name = params[:user][:name]
+ @user_name = params[:user][:username]
@project_name = params[:project_name]
@project_url = params[:project_url]
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index d9ce5088903..79d041d2775 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -33,18 +33,24 @@ class ProjectTeam
member
end
- def add_users(users, access, current_user: nil, expires_at: nil)
+ def add_users(users, access_level, current_user: nil, expires_at: nil)
ProjectMember.add_users_to_projects(
[project.id],
users,
- access,
+ access_level,
current_user: current_user,
expires_at: expires_at
)
end
- def add_user(user, access, current_user: nil, expires_at: nil)
- add_users([user], access, current_user: current_user, expires_at: expires_at)
+ def add_user(user, access_level, current_user: nil, expires_at: nil)
+ ProjectMember.add_user(
+ project,
+ user,
+ access_level,
+ current_user: current_user,
+ expires_at: expires_at
+ )
end
# Remove all users from project team
diff --git a/app/models/release.rb b/app/models/release.rb
index e196b84eb18..c936899799e 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -1,4 +1,8 @@
class Release < ActiveRecord::Base
+ include CacheMarkdownField
+
+ cache_markdown_field :description
+
belongs_to :project
validates :description, :project, :tag, presence: true
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 51557228ab9..4da1933c189 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -111,8 +111,10 @@ class Repository
def find_commits_by_message(query, ref = nil, path = nil, limit = 1000, offset = 0)
ref ||= root_ref
- # Limited to 1000 commits for now, could be parameterized?
- args = %W(#{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset} --max-count #{limit} --grep=#{query})
+ args = %W(
+ #{Gitlab.config.git.bin_path} log #{ref} --pretty=%H --skip #{offset}
+ --max-count #{limit} --grep=#{query} --regexp-ignore-case
+ )
args = args.concat(%W(-- #{path})) if path.present?
git_log_results = Gitlab::Popen.popen(args, path_to_repo).first.lines.map(&:chomp)
@@ -838,6 +840,52 @@ class Repository
end
end
+ def multi_action(user:, branch:, message:, actions:, author_email: nil, author_name: nil)
+ update_branch_with_hooks(user, branch) do |ref|
+ index = rugged.index
+ parents = []
+ branch = find_branch(ref)
+
+ if branch
+ last_commit = branch.target
+ index.read_tree(last_commit.raw_commit.tree)
+ parents = [last_commit.sha]
+ end
+
+ actions.each do |action|
+ case action[:action]
+ when :create, :update, :move
+ mode =
+ case action[:action]
+ when :update
+ index.get(action[:file_path])[:mode]
+ when :move
+ index.get(action[:previous_path])[:mode]
+ end
+ mode ||= 0o100644
+
+ index.remove(action[:previous_path]) if action[:action] == :move
+
+ content = action[:encoding] == 'base64' ? Base64.decode64(action[:content]) : action[:content]
+ oid = rugged.write(content, :blob)
+
+ index.add(path: action[:file_path], oid: oid, mode: mode)
+ when :delete
+ index.remove(action[:file_path])
+ end
+ end
+
+ options = {
+ tree: index.write_tree(rugged),
+ message: message,
+ parents: parents
+ }
+ options.merge!(get_committer_and_author(user, email: author_email, name: author_name))
+
+ Rugged::Commit.create(rugged, options)
+ end
+ end
+
def get_committer_and_author(user, email: nil, name: nil)
committer = user_to_committer(user)
author = Gitlab::Git::committer_hash(email: email, name: name) || committer
@@ -997,6 +1045,10 @@ class Repository
Gitlab::Popen.popen(args, path_to_repo)
end
+ def create_ref(ref, ref_path)
+ fetch_ref(path_to_repo, ref, ref_path)
+ end
+
def update_branch_with_hooks(current_user, branch)
update_autocrlf_option
diff --git a/app/models/service.rb b/app/models/service.rb
index 80de7175565..66c804f2b06 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -136,6 +136,7 @@ class Service < ActiveRecord::Base
end
def #{arg}=(value)
+ self.properties ||= {}
updated_properties['#{arg}'] = #{arg} unless #{arg}_changed?
self.properties['#{arg}'] = value
end
diff --git a/app/models/snippet.rb b/app/models/snippet.rb
index 8a1730f3f36..2373b445009 100644
--- a/app/models/snippet.rb
+++ b/app/models/snippet.rb
@@ -1,11 +1,21 @@
class Snippet < ActiveRecord::Base
include Gitlab::VisibilityLevel
include Linguist::BlobHelper
+ include CacheMarkdownField
include Participable
include Referable
include Sortable
include Awardable
+ cache_markdown_field :title, pipeline: :single_line
+ cache_markdown_field :content
+
+ # If file_name changes, it invalidates content
+ alias_method :default_content_html_invalidator, :content_html_invalidated?
+ def content_html_invalidated?
+ default_content_html_invalidator || file_name_changed?
+ end
+
default_value_for :visibility_level, Snippet::PRIVATE
belongs_to :author, class_name: 'User'
diff --git a/app/models/trending_project.rb b/app/models/trending_project.rb
new file mode 100644
index 00000000000..27e3732da17
--- /dev/null
+++ b/app/models/trending_project.rb
@@ -0,0 +1,35 @@
+class TrendingProject < ActiveRecord::Base
+ belongs_to :project
+
+ # The number of months to include in the trending calculation.
+ MONTHS_TO_INCLUDE = 1
+
+ # The maximum number of projects to include in the trending set.
+ PROJECTS_LIMIT = 100
+
+ # Populates the trending projects table with the current list of trending
+ # projects.
+ def self.refresh!
+ # The calculation **must** run in a transaction. If the removal of data and
+ # insertion of new data were to run separately a user might end up with an
+ # empty list of trending projects for a short period of time.
+ transaction do
+ delete_all
+
+ timestamp = connection.quote(MONTHS_TO_INCLUDE.months.ago)
+
+ connection.execute <<-EOF.strip_heredoc
+ INSERT INTO #{table_name} (project_id)
+ SELECT project_id
+ FROM notes
+ INNER JOIN projects ON projects.id = notes.project_id
+ WHERE notes.created_at >= #{timestamp}
+ AND notes.system IS FALSE
+ AND projects.visibility_level = #{Gitlab::VisibilityLevel::PUBLIC}
+ GROUP BY project_id
+ ORDER BY count(*) DESC
+ LIMIT #{PROJECTS_LIMIT};
+ EOF
+ end
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 6996740eebd..f367f4616fb 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -279,6 +279,11 @@ class User < ActiveRecord::Base
find_by('users.username = ? OR users.id = ?', name_or_id.to_s, name_or_id.to_i)
end
+ # Returns a user for the given SSH key.
+ def find_by_ssh_key_id(key_id)
+ find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
+ end
+
def build_user(attrs = {})
User.new(attrs)
end
@@ -584,6 +589,11 @@ class User < ActiveRecord::Base
end
def set_projects_limit
+ # `User.select(:id)` raises
+ # `ActiveModel::MissingAttributeError: missing attribute: projects_limit`
+ # without this safeguard!
+ return unless self.has_attribute?(:projects_limit)
+
connection_default_value_defined = new_record? && !projects_limit_changed?
return unless self.projects_limit.nil? || connection_default_value_defined
@@ -827,6 +837,22 @@ class User < ActiveRecord::Base
todos_pending_count(force: true)
end
+ # This is copied from Devise::Models::Lockable#valid_for_authentication?, as our auth
+ # flow means we don't call that automatically (and can't conveniently do so).
+ #
+ # See:
+ # <https://github.com/plataformatec/devise/blob/v4.0.0/lib/devise/models/lockable.rb#L92>
+ #
+ def increment_failed_attempts!
+ self.failed_attempts ||= 0
+ self.failed_attempts += 1
+ if attempts_exceeded?
+ lock_access! unless access_locked?
+ else
+ save(validate: false)
+ end
+ end
+
private
def projects_union(min_access_level = nil)
@@ -881,7 +907,7 @@ class User < ActiveRecord::Base
if domain_matches?(allowed_domains, self.email)
valid = true
else
- error = "is not whitelisted. Email domains valid for registration are: #{allowed_domains.join(', ')}"
+ error = "domain is not authorized for sign-up"
valid = false
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index be25c750d67..a806cf83782 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -98,7 +98,6 @@ class ProjectPolicy < BasePolicy
can! :admin_milestone
can! :admin_project_snippet
can! :admin_project_member
- can! :admin_merge_request
can! :admin_note
can! :admin_wiki
can! :admin_project
@@ -139,11 +138,18 @@ class ProjectPolicy < BasePolicy
def team_access!(user)
access = project.team.max_member_access(user.id)
- guest_access! if access >= Gitlab::Access::GUEST
- reporter_access! if access >= Gitlab::Access::REPORTER
- team_member_reporter_access! if access >= Gitlab::Access::REPORTER
- developer_access! if access >= Gitlab::Access::DEVELOPER
- master_access! if access >= Gitlab::Access::MASTER
+ return if access < Gitlab::Access::GUEST
+ guest_access!
+
+ return if access < Gitlab::Access::REPORTER
+ reporter_access!
+ team_member_reporter_access!
+
+ return if access < Gitlab::Access::DEVELOPER
+ developer_access!
+
+ return if access < Gitlab::Access::MASTER
+ master_access!
end
def archived_access!
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 38ac6631228..8ea88da8a53 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -7,10 +7,10 @@ module Auth
def execute(authentication_abilities:)
@authentication_abilities = authentication_abilities
- return error('not found', 404) unless registry.enabled
+ return error('UNAVAILABLE', status: 404, message: 'registry not enabled') unless registry.enabled
unless current_user || project
- return error('forbidden', 403) unless scope
+ return error('DENIED', status: 403, message: 'access forbidden') unless scope
end
{ token: authorized_token(scope).encoded }
@@ -111,5 +111,12 @@ module Auth
@authentication_abilities.include?(:create_container_image) &&
can?(current_user, :create_container_image, requested_project)
end
+
+ def error(code, status:, message: '')
+ {
+ errors: [{ code: code, message: message }],
+ http_status: status
+ }
+ end
end
end
diff --git a/app/services/base_service.rb b/app/services/base_service.rb
index 0c208150fb8..1a2bad77a02 100644
--- a/app/services/base_service.rb
+++ b/app/services/base_service.rb
@@ -56,9 +56,8 @@ class BaseService
result
end
- def success
- {
- status: :success
- }
+ def success(pass_back = {})
+ pass_back[:status] = :success
+ pass_back
end
end
diff --git a/app/services/boards/issues/create_service.rb b/app/services/boards/issues/create_service.rb
new file mode 100644
index 00000000000..3701afd441f
--- /dev/null
+++ b/app/services/boards/issues/create_service.rb
@@ -0,0 +1,16 @@
+module Boards
+ module Issues
+ class CreateService < Boards::BaseService
+ def execute(list)
+ params.merge!(label_ids: [list.label_id])
+ create_issue
+ end
+
+ private
+
+ def create_issue
+ ::Issues::CreateService.new(project, current_user, params).execute
+ end
+ end
+ end
+end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 34efd09ed9f..435a8c6e681 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -36,12 +36,7 @@ module Boards
end
def set_state
- params[:state] =
- case list.list_type.to_sym
- when :backlog then 'opened'
- when :done then 'closed'
- else 'all'
- end
+ params[:state] = list.done? ? 'closed' : 'opened'
end
def board_label_ids
diff --git a/app/services/boards/lists/generate_service.rb b/app/services/boards/lists/generate_service.rb
index 1c48b9786e4..830e386c98b 100644
--- a/app/services/boards/lists/generate_service.rb
+++ b/app/services/boards/lists/generate_service.rb
@@ -25,10 +25,8 @@ module Boards
def label_params
[
- { name: 'Development', color: '#5CB85C' },
- { name: 'Testing', color: '#F0AD4E' },
- { name: 'Production', color: '#FF5F00' },
- { name: 'Ready', color: '#FF0000' }
+ { name: 'To Do', color: '#F0AD4E' },
+ { name: 'Doing', color: '#5CB85C' }
]
end
end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 36c93dddadb..d3dd30b2588 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -16,6 +16,8 @@ module Ci
process_stage(index)
end
+ @pipeline.update_status
+
# Return a flag if a when builds got enqueued
new_builds.flatten.any?
end
diff --git a/app/services/files/base_service.rb b/app/services/files/base_service.rb
index e8465729d06..9bd4bd464f7 100644
--- a/app/services/files/base_service.rb
+++ b/app/services/files/base_service.rb
@@ -27,8 +27,9 @@ module Files
create_target_branch
end
- if commit
- success
+ result = commit
+ if result
+ success(result: result)
else
error('Something went wrong. Your changes were not committed')
end
@@ -42,6 +43,12 @@ module Files
@source_branch != @target_branch || @source_project != @project
end
+ def file_has_changed?
+ return false unless @last_commit_sha && last_commit
+
+ @last_commit_sha != last_commit.sha
+ end
+
def raise_error(message)
raise ValidationError.new(message)
end
diff --git a/app/services/files/multi_service.rb b/app/services/files/multi_service.rb
new file mode 100644
index 00000000000..d28912e1301
--- /dev/null
+++ b/app/services/files/multi_service.rb
@@ -0,0 +1,124 @@
+require_relative "base_service"
+
+module Files
+ class MultiService < Files::BaseService
+ class FileChangedError < StandardError; end
+
+ def commit
+ repository.multi_action(
+ user: current_user,
+ branch: @target_branch,
+ message: @commit_message,
+ actions: params[:actions],
+ author_email: @author_email,
+ author_name: @author_name
+ )
+ end
+
+ private
+
+ def validate
+ super
+
+ params[:actions].each_with_index do |action, index|
+ unless action[:file_path].present?
+ raise_error("You must specify a file_path.")
+ end
+
+ regex_check(action[:file_path])
+ regex_check(action[:previous_path]) if action[:previous_path]
+
+ if project.empty_repo? && action[:action] != :create
+ raise_error("No files to #{action[:action]}.")
+ end
+
+ validate_file_exists(action)
+
+ case action[:action]
+ when :create
+ validate_create(action)
+ when :update
+ validate_update(action)
+ when :delete
+ validate_delete(action)
+ when :move
+ validate_move(action, index)
+ else
+ raise_error("Unknown action type `#{action[:action]}`.")
+ end
+ end
+ end
+
+ def validate_file_exists(action)
+ return if action[:action] == :create
+
+ file_path = action[:file_path]
+ file_path = action[:previous_path] if action[:action] == :move
+
+ blob = repository.blob_at_branch(params[:branch_name], file_path)
+
+ unless blob
+ raise_error("File to be #{action[:action]}d `#{file_path}` does not exist.")
+ end
+ end
+
+ def last_commit
+ Gitlab::Git::Commit.last_for_path(repository, @source_branch, @file_path)
+ end
+
+ def regex_check(file)
+ if file =~ Gitlab::Regex.directory_traversal_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name, `' +
+ file +
+ '` ' +
+ Gitlab::Regex.directory_traversal_regex_message
+ )
+ end
+
+ unless file =~ Gitlab::Regex.file_path_regex
+ raise_error(
+ 'Your changes could not be committed, because the file name, `' +
+ file +
+ '` ' +
+ Gitlab::Regex.file_path_regex_message
+ )
+ end
+ end
+
+ def validate_create(action)
+ return if project.empty_repo?
+
+ if repository.blob_at_branch(params[:branch_name], action[:file_path])
+ raise_error("Your changes could not be committed because a file with the name `#{action[:file_path]}` already exists.")
+ end
+ end
+
+ def validate_delete(action)
+ end
+
+ def validate_move(action, index)
+ if action[:previous_path].nil?
+ raise_error("You must supply the original file path when moving file `#{action[:file_path]}`.")
+ end
+
+ blob = repository.blob_at_branch(params[:branch_name], action[:file_path])
+
+ if blob
+ raise_error("Move destination `#{action[:file_path]}` already exists.")
+ end
+
+ if action[:content].nil?
+ blob = repository.blob_at_branch(params[:branch_name], action[:previous_path])
+ blob.load_all_data!(repository) if blob.truncated?
+ params[:actions][index][:content] = blob.data
+ end
+ end
+
+ def validate_update(action)
+ if file_has_changed?
+ raise FileChangedError.new("You are attempting to update a file `#{action[:file_path]}` that has changed since you started editing it.")
+ end
+ end
+ end
+end
diff --git a/app/services/files/update_service.rb b/app/services/files/update_service.rb
index 9e9b5b63f26..c17fdb8d1f1 100644
--- a/app/services/files/update_service.rb
+++ b/app/services/files/update_service.rb
@@ -23,12 +23,6 @@ module Files
end
end
- def file_has_changed?
- return false unless @last_commit_sha && last_commit
-
- @last_commit_sha != last_commit.sha
- end
-
def last_commit
@last_commit ||= Gitlab::Git::Commit.
last_for_path(@source_project.repository, @source_branch, @file_path)
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index fbce46769f7..57d521f2fea 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -50,6 +50,7 @@ class IssuableBaseService < BaseService
params.delete(:remove_label_ids)
params.delete(:label_ids)
params.delete(:assignee_id)
+ params.delete(:due_date)
end
end
diff --git a/app/services/members/approve_access_request_service.rb b/app/services/members/approve_access_request_service.rb
new file mode 100644
index 00000000000..416aee2ab51
--- /dev/null
+++ b/app/services/members/approve_access_request_service.rb
@@ -0,0 +1,31 @@
+module Members
+ class ApproveAccessRequestService < BaseService
+ include MembersHelper
+
+ attr_accessor :source
+
+ def initialize(source, current_user, params = {})
+ @source = source
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ 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)
+
+ access_requester.access_level = params[:access_level] if params[:access_level]
+ access_requester.accept_request
+
+ access_requester
+ end
+
+ private
+
+ def can_update_access_requester?(access_requester)
+ access_requester && can?(current_user, action_member_permission(:update, 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
index ca9db59cac7..b7a244c2029 100644
--- a/app/services/members/authorized_destroy_service.rb
+++ b/app/services/members/authorized_destroy_service.rb
@@ -14,6 +14,8 @@ module Members
if member.request? && member.user != user
notification_service.decline_access_request(member)
end
+
+ member
end
end
end
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index 9a2bf82ef51..431da8372c9 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -1,17 +1,42 @@
module Members
class DestroyService < BaseService
- attr_accessor :member, :current_user
+ include MembersHelper
- def initialize(member, current_user)
- @member = member
+ attr_accessor :source
+
+ ALLOWED_SCOPES = %i[members requesters all]
+
+ def initialize(source, current_user, params = {})
+ @source = source
@current_user = current_user
+ @params = params
end
- def execute
- unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
- raise Gitlab::Access::AccessDeniedError
- end
+ def execute(scope = :members)
+ raise "scope :#{scope} is not allowed!" unless ALLOWED_SCOPES.include?(scope)
+
+ member = find_member!(scope)
+
+ raise Gitlab::Access::AccessDeniedError unless can_destroy_member?(member)
+
AuthorizedDestroyService.new(member, current_user).execute
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)
+ end
+ end
+
+ def can_destroy_member?(member)
+ member && can?(current_user, action_member_permission(:destroy, member), member)
+ end
end
end
diff --git a/app/services/members/request_access_service.rb b/app/services/members/request_access_service.rb
new file mode 100644
index 00000000000..2614153d900
--- /dev/null
+++ b/app/services/members/request_access_service.rb
@@ -0,0 +1,25 @@
+module Members
+ class RequestAccessService < BaseService
+ attr_accessor :source
+
+ def initialize(source, current_user)
+ @source = source
+ @current_user = current_user
+ end
+
+ def execute
+ raise Gitlab::Access::AccessDeniedError unless can_request_access?(source)
+
+ source.members.create(
+ access_level: Gitlab::Access::DEVELOPER,
+ user: current_user,
+ requested_at: Time.now.utc)
+ end
+
+ private
+
+ def can_request_access?(source)
+ source && can?(current_user, :request_access, source)
+ end
+ end
+end
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index ba424b09463..d0d155b7ee1 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -5,16 +5,17 @@ module MergeRequests
end
def create_title_change_note(issuable, old_title)
- removed_wip = old_title =~ MergeRequest::WIP_REGEX && !issuable.work_in_progress?
- added_wip = old_title !~ MergeRequest::WIP_REGEX && issuable.work_in_progress?
+ removed_wip = MergeRequest.work_in_progress?(old_title) && !issuable.work_in_progress?
+ added_wip = !MergeRequest.work_in_progress?(old_title) && issuable.work_in_progress?
+ changed_title = MergeRequest.wipless_title(old_title) != issuable.wipless_title
if removed_wip
SystemNoteService.remove_merge_request_wip(issuable, issuable.project, current_user)
elsif added_wip
SystemNoteService.add_merge_request_wip(issuable, issuable.project, current_user)
- else
- super
end
+
+ super if changed_title
end
def hook_data(merge_request, action, oldrev = nil)
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index e57791f6818..404f75616b5 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -4,7 +4,7 @@ module MergeRequests
merge_request = MergeRequest.new(params)
# Set MR attributes
- merge_request.can_be_created = false
+ merge_request.can_be_created = true
merge_request.compare_commits = []
merge_request.source_project = project unless merge_request.source_project
@@ -22,6 +22,12 @@ module MergeRequests
return build_failed(merge_request, message)
end
+ if merge_request.source_project == merge_request.target_project &&
+ merge_request.target_branch == merge_request.source_branch
+
+ return build_failed(merge_request, 'You must select different branches')
+ end
+
compare = CompareService.new.execute(
merge_request.source_project,
merge_request.source_branch,
@@ -29,17 +35,8 @@ module MergeRequests
merge_request.target_branch,
)
- commits = compare.commits
-
- # At this point we decide if merge request can be created
- # If we have at least one commit to merge -> creation allowed
- if commits.present?
- merge_request.compare_commits = commits
- merge_request.can_be_created = true
- merge_request.compare = compare
- else
- merge_request.can_be_created = false
- end
+ merge_request.compare_commits = compare.commits
+ merge_request.compare = compare
set_title_and_description(merge_request)
end
@@ -89,6 +86,8 @@ module MergeRequests
end
end
+ merge_request.title = merge_request.wip_title if commits.empty?
+
merge_request
end
diff --git a/app/services/merge_requests/post_merge_service.rb b/app/services/merge_requests/post_merge_service.rb
index 8437d9b8b43..e8fb1b59752 100644
--- a/app/services/merge_requests/post_merge_service.rb
+++ b/app/services/merge_requests/post_merge_service.rb
@@ -7,6 +7,7 @@ module MergeRequests
class PostMergeService < MergeRequests::BaseService
def execute(merge_request)
close_issues(merge_request)
+ todo_service.merge_merge_request(merge_request, current_user)
merge_request.mark_as_merged
create_merge_event(merge_request, current_user)
create_note(merge_request)
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index f14f9e4b327..9dbec49d163 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -16,7 +16,7 @@ module MergeRequests
end
merge_request.merge_params['force_remove_source_branch'] = params.delete(:force_remove_source_branch)
-
+ handle_wip_event(merge_request)
update(merge_request)
end
@@ -81,5 +81,18 @@ module MergeRequests
def after_update(issuable)
issuable.cache_merge_request_closes_issues!(current_user)
end
+
+ 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
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 6139ed56e25..de8049b8e2e 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -134,7 +134,8 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- :merged_merge_request_email
+ :merged_merge_request_email,
+ skip_current_user: !merge_request.merge_when_build_succeeds?
)
end
@@ -514,9 +515,16 @@ class NotificationService
end
end
- def close_resource_email(target, project, current_user, method)
+ def close_resource_email(target, project, current_user, method, skip_current_user: true)
action = method == :merged_merge_request_email ? "merge" : "close"
- recipients = build_recipients(target, project, current_user, action: action)
+
+ recipients = build_recipients(
+ target,
+ project,
+ current_user,
+ action: action,
+ skip_current_user: skip_current_user
+ )
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
@@ -557,7 +565,7 @@ class NotificationService
end
end
- def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
+ def build_recipients(target, project, current_user, action: nil, previous_assignee: nil, skip_current_user: true)
custom_action = build_custom_key(action, target)
recipients = target.participants(current_user)
@@ -586,7 +594,8 @@ class NotificationService
recipients = reject_unsubscribed_users(recipients, target)
recipients = reject_users_without_access(recipients, target)
- recipients.delete(current_user)
+ recipients.delete(current_user) if skip_current_user
+
recipients.uniq
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index be749ba4a1c..15d7918e7fd 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -7,6 +7,8 @@ module Projects
def execute
forked_from_project_id = params.delete(:forked_from_project_id)
import_data = params.delete(:import_data)
+ @skip_wiki = params.delete(:skip_wiki)
+
@project = Project.new(params)
# Make sure that the user is allowed to use the specified visibility level
@@ -15,6 +17,11 @@ module Projects
return @project
end
+ unless allowed_fork?(forked_from_project_id)
+ @project.errors.add(:forked_from_project_id, 'is forbidden')
+ return @project
+ end
+
# Set project name from path
if @project.name.present? && @project.path.present?
# if both name and path set - everything is ok
@@ -71,6 +78,13 @@ module Projects
@project.errors.add(:namespace, "is not valid")
end
+ def allowed_fork?(source_project_id)
+ return true if source_project_id.nil?
+
+ source_project = Project.find_by(id: source_project_id)
+ current_user.can?(:fork_project, source_project)
+ end
+
def allowed_namespace?(user, namespace_id)
namespace = Namespace.find_by(id: namespace_id)
current_user.can?(:create_projects, namespace)
@@ -80,7 +94,7 @@ module Projects
log_info("#{@project.owner.name} created a new project \"#{@project.name_with_namespace}\"")
unless @project.gitlab_project_import?
- @project.create_wiki if @project.feature_available?(:wiki, current_user)
+ @project.create_wiki unless skip_wiki?
@project.build_missing_services
@project.create_labels
@@ -94,6 +108,10 @@ module Projects
end
end
+ def skip_wiki?
+ !@project.feature_available?(:wiki, current_user) || @skip_wiki
+ end
+
def save_project_and_import_data(import_data)
Project.transaction do
@project.create_or_update_import_data(data: import_data[:data], credentials: import_data[:credentials]) if import_data
diff --git a/app/services/projects/fork_service.rb b/app/services/projects/fork_service.rb
index a2de4dccece..a2b23ea6171 100644
--- a/app/services/projects/fork_service.rb
+++ b/app/services/projects/fork_service.rb
@@ -16,6 +16,8 @@ module Projects
end
new_project = CreateService.new(current_user, new_params).execute
+ return new_project unless new_project.persisted?
+
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update_attributes(builds_access_level: builds_access_level)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index cdad0426b02..e466ffa60eb 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -44,6 +44,11 @@ module Projects
begin
gitlab_shell.import_repository(project.repository_storage_path, project.path_with_namespace, project.import_url)
rescue => e
+ # Expire cache to prevent scenarios such as:
+ # 1. First import failed, but the repo was imported successfully, so +exists?+ returns true
+ # 2. Retried import, repo is broken or not imported but +exists?+ still returns true
+ project.repository.before_import if project.repository_exists?
+
raise Error, "Error importing repository #{project.import_url} into #{project.path_with_namespace} - #{e.message}"
end
end
diff --git a/app/services/slash_commands/interpret_service.rb b/app/services/slash_commands/interpret_service.rb
index 9ac1124abc1..1725a30fae5 100644
--- a/app/services/slash_commands/interpret_service.rb
+++ b/app/services/slash_commands/interpret_service.rb
@@ -195,7 +195,7 @@ module SlashCommands
params '<in 2 days | this Friday | December 31st>'
condition do
issuable.respond_to?(:due_date) &&
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :due do |due_date_param|
due_date = Chronic.parse(due_date_param).try(:to_date)
@@ -208,12 +208,24 @@ module SlashCommands
issuable.persisted? &&
issuable.respond_to?(:due_date) &&
issuable.due_date? &&
- current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ current_user.can?(:"admin_#{issuable.to_ability_name}", project)
end
command :remove_due_date do
@updates[:due_date] = nil
end
+ desc do
+ "Toggle the Work In Progress status"
+ end
+ condition do
+ issuable.persisted? &&
+ issuable.respond_to?(:work_in_progress?) &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :wip do
+ @updates[:wip_event] = issuable.work_in_progress? ? 'unwip' : 'wip'
+ end
+
# This is a dummy command, so that it appears in the autocomplete commands
desc 'CC'
params '@user'
diff --git a/app/services/system_hooks_service.rb b/app/services/system_hooks_service.rb
index 1fb72cf89e9..a2bfa422c9d 100644
--- a/app/services/system_hooks_service.rb
+++ b/app/services/system_hooks_service.rb
@@ -72,7 +72,7 @@ class SystemHooksService
return 'user_add_to_group' if event == :create
return 'user_remove_from_group' if event == :destroy
else
- "#{model.class.name.downcase}_#{event.to_s}"
+ "#{model.class.name.downcase}_#{event}"
end
end
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index 0c8446e7c3d..1ce66d50368 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -24,6 +24,7 @@ module SystemNoteService
body = "Added #{commits_text}:\n\n"
body << existing_commit_summary(noteable, existing_commits, oldrev)
body << new_commit_summary(new_commits).join("\n")
+ body << "\n\n[Compare with previous version](#{diff_comparison_url(noteable, project, oldrev)})"
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -245,7 +246,7 @@ module SystemNoteService
'deleted'
end
- body = "#{verb} #{branch_type.to_s} branch `#{branch}`".capitalize
+ body = "#{verb} #{branch_type} branch `#{branch}`".capitalize
create_note(noteable: noteable, project: project, author: author, note: body)
end
@@ -254,8 +255,7 @@ module SystemNoteService
#
# "Started branch `201-issue-branch-button`"
def new_issue_branch(issue, project, author, branch)
- h = Gitlab::Routing.url_helpers
- link = h.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
+ link = url_helpers.namespace_project_compare_url(project.namespace, project, from: project.default_branch, to: branch)
body = "Started branch [`#{branch}`](#{link})"
create_note(noteable: issue, project: project, author: author, note: body)
@@ -347,7 +347,7 @@ module SystemNoteService
notes = notes.where(noteable_id: noteable.id)
end
- notes_for_mentioner(mentioner, noteable, notes).count > 0
+ notes_for_mentioner(mentioner, noteable, notes).exists?
end
# Build an Array of lines detailing each commit added in a merge request
@@ -466,4 +466,20 @@ module SystemNoteService
def escape_html(text)
Rack::Utils.escape_html(text)
end
+
+ def url_helpers
+ @url_helpers ||= Gitlab::Routing.url_helpers
+ end
+
+ def diff_comparison_url(merge_request, project, oldrev)
+ diff_id = merge_request.merge_request_diff.id
+
+ url_helpers.diffs_namespace_project_merge_request_url(
+ project.namespace,
+ project,
+ merge_request.iid,
+ diff_id: diff_id,
+ start_sha: oldrev
+ )
+ end
end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
index 7a35958cc5f..2821ecf0a88 100644
--- a/app/validators/namespace_validator.rb
+++ b/app/validators/namespace_validator.rb
@@ -5,7 +5,8 @@
# Values are checked for formatting and exclusion from a list of reserved path
# names.
class NamespaceValidator < ActiveModel::EachValidator
- RESERVED = %w(
+ RESERVED = %w[
+ .well-known
admin
all
assets
@@ -23,6 +24,7 @@ class NamespaceValidator < ActiveModel::EachValidator
projects
public
repository
+ robots.txt
s
search
services
@@ -31,7 +33,7 @@ class NamespaceValidator < ActiveModel::EachValidator
u
unsubscribes
users
- ).freeze
+ ].freeze
def validate_each(record, attribute, value)
unless value =~ Gitlab::Regex.namespace_regex
diff --git a/app/views/admin/abuse_reports/_abuse_report.html.haml b/app/views/admin/abuse_reports/_abuse_report.html.haml
index 56bf6194914..05f3d9a3b50 100644
--- a/app/views/admin/abuse_reports/_abuse_report.html.haml
+++ b/app/views/admin/abuse_reports/_abuse_report.html.haml
@@ -21,7 +21,7 @@
%td
%strong.subheading.visible-xs-block.visible-sm-block Message
.message
- = markdown(abuse_report.message.squish!, pipeline: :single_line, author: reporter)
+ = markdown_field(abuse_report, :message)
%td
- if user
= link_to 'Remove user & report', admin_abuse_report_path(abuse_report, remove_user: true),
diff --git a/app/views/admin/background_jobs/_head.html.haml b/app/views/admin/background_jobs/_head.html.haml
index 107fc25244a..b3530915068 100644
--- a/app/views/admin/background_jobs/_head.html.haml
+++ b/app/views/admin/background_jobs/_head.html.haml
@@ -1,24 +1,25 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :system_info) do
- = link_to admin_system_info_path, title: 'System Info' do
- %span
- System Info
- = nav_link(controller: :background_jobs) do
- = link_to admin_background_jobs_path, title: 'Background Jobs' do
- %span
- Background Jobs
- = nav_link(controller: :logs) do
- = link_to admin_logs_path, title: 'Logs' do
- %span
- Logs
- = nav_link(controller: :health_check) do
- = link_to admin_health_check_path, title: 'Health Check' do
- %span
- Health Check
- = nav_link(controller: :requests_profiles) do
- = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
- %span
- Requests Profiles
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :system_info) do
+ = link_to admin_system_info_path, title: 'System Info' do
+ %span
+ System Info
+ = nav_link(controller: :background_jobs) do
+ = link_to admin_background_jobs_path, title: 'Background Jobs' do
+ %span
+ Background Jobs
+ = nav_link(controller: :logs) do
+ = link_to admin_logs_path, title: 'Logs' do
+ %span
+ Logs
+ = nav_link(controller: :health_check) do
+ = link_to admin_health_check_path, title: 'Health Check' do
+ %span
+ Health Check
+ = nav_link(controller: :requests_profiles) do
+ = link_to admin_requests_profiles_path, title: 'Requests Profiles' do
+ %span
+ Requests Profiles
diff --git a/app/views/admin/broadcast_messages/_form.html.haml b/app/views/admin/broadcast_messages/_form.html.haml
index 6b157abf842..3132d157f29 100644
--- a/app/views/admin/broadcast_messages/_form.html.haml
+++ b/app/views/admin/broadcast_messages/_form.html.haml
@@ -1,7 +1,10 @@
.broadcast-message-preview{ style: broadcast_message_style(@broadcast_message) }
= icon('bullhorn')
.js-broadcast-message-preview
- = render_broadcast_message(@broadcast_message.message.presence || "Your message here")
+ - if @broadcast_message.message.present?
+ = render_broadcast_message(@broadcast_message)
+ - else
+ = "Your message here"
= form_for [:admin, @broadcast_message], html: { class: 'broadcast-message-form form-horizontal js-quick-submit js-requires-input'} do |f|
= form_errors(@broadcast_message)
@@ -18,11 +21,11 @@
.form-group.js-toggle-colors-container.hide
= f.label :color, "Background Color", class: 'control-label'
.col-sm-10
- = f.color_field :color, class: "form-control"
+ = f.text_field :color, class: "form-control"
.form-group.js-toggle-colors-container.hide
= f.label :font, "Font Color", class: 'control-label'
.col-sm-10
- = f.color_field :font, class: "form-control"
+ = f.text_field :font, class: "form-control"
.form-group
= f.label :starts_at, class: 'control-label'
.col-sm-10.datetime-controls
diff --git a/app/views/admin/broadcast_messages/preview.js.haml b/app/views/admin/broadcast_messages/preview.js.haml
index fbc9453c72e..c72e59640d7 100644
--- a/app/views/admin/broadcast_messages/preview.js.haml
+++ b/app/views/admin/broadcast_messages/preview.js.haml
@@ -1 +1 @@
-$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@message))}");
+$('.js-broadcast-message-preview').html("#{j(render_broadcast_message(@broadcast_message))}");
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index c91ab4cb946..ec40391a3e3 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -1,28 +1,29 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview' do
- %span
- Overview
- = nav_link(controller: [:admin, :projects]) do
- = link_to admin_namespaces_projects_path, title: 'Projects' do
- %span
- Projects
- = nav_link(controller: :users) do
- = link_to admin_users_path, title: 'Users' do
- %span
- Users
- = nav_link(controller: :groups) do
- = link_to admin_groups_path, title: 'Groups' do
- %span
- Groups
- = nav_link path: 'builds#index' do
- = link_to admin_builds_path, title: 'Builds' do
- %span
- Builds
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- %span
- Runners
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: :dashboard, html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview' do
+ %span
+ Overview
+ = nav_link(controller: [:admin, :projects]) do
+ = link_to admin_namespaces_projects_path, title: 'Projects' do
+ %span
+ Projects
+ = nav_link(controller: :users) do
+ = link_to admin_users_path, title: 'Users' do
+ %span
+ Users
+ = nav_link(controller: :groups) do
+ = link_to admin_groups_path, title: 'Groups' do
+ %span
+ Groups
+ = nav_link path: 'builds#index' do
+ = link_to admin_builds_path, title: 'Builds' do
+ %span
+ Builds
+ = nav_link path: ['runners#index', 'runners#show'] do
+ = link_to admin_runners_path, title: 'Runners' do
+ %span
+ Runners
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index e6687f43816..90798c47d97 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -63,6 +63,11 @@
Reply by email
%span.light.pull-right
= boolean_to_icon Gitlab::IncomingEmail.enabled?
+ %p
+ Container Registry
+ %span.light.pull-right
+ = boolean_to_icon Gitlab.config.registry.enabled
+
.col-md-4
%h4
Components
diff --git a/app/views/admin/groups/_group.html.haml b/app/views/admin/groups/_group.html.haml
index 77a11e49e20..adfa1eaafc9 100644
--- a/app/views/admin/groups/_group.html.haml
+++ b/app/views/admin/groups/_group.html.haml
@@ -23,4 +23,4 @@
- if group.description.present?
.description
- = markdown(group.description, pipeline: :description)
+ = markdown_field(group, :description)
diff --git a/app/views/admin/labels/_form.html.haml b/app/views/admin/labels/_form.html.haml
index 602cfa9b6fc..d5e6bede36a 100644
--- a/app/views/admin/labels/_form.html.haml
+++ b/app/views/admin/labels/_form.html.haml
@@ -14,7 +14,7 @@
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
- = f.color_field :color, class: "form-control"
+ = f.text_field :color, class: "form-control"
.help-block
Choose any color.
%br
diff --git a/app/views/admin/labels/_label.html.haml b/app/views/admin/labels/_label.html.haml
index f417b2e44a4..be224d66855 100644
--- a/app/views/admin/labels/_label.html.haml
+++ b/app/views/admin/labels/_label.html.haml
@@ -1,7 +1,7 @@
%li{id: dom_id(label)}
.label-row
= render_colored_label(label, tooltip: false)
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
.pull-right
= link_to 'Edit', edit_admin_label_path(label), class: 'btn btn-sm'
= link_to 'Delete', admin_label_path(label), class: 'btn btn-sm btn-remove remove-row', method: :delete, remote: true, data: {confirm: "Delete this label? Are you sure?"}
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 1e755785d90..339cfc613fe 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -87,7 +87,7 @@
- if project.description.present?
.description
- = markdown(project.description, pipeline: :description)
+ = markdown_field(project, :description)
= paginate @projects, theme: 'gitlab'
- else
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index a53876d6757..b760b42fde0 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -5,8 +5,10 @@
%p.prepend-top-default
%span
- To register a new runner you should enter the following registration token.
- With this token the runner will request a unique runner token and use that for future communication.
+ To register a new Runner you should enter the following registration
+ token.
+ With this token the Runner will request a unique Runner token and use
+ that for future communication.
%br
Registration token is
%code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
@@ -24,27 +26,27 @@
.bs-callout
%p
- A 'runner' is a process which runs a build.
- You can setup as many runners as you need.
+ A 'Runner' is a process which runs a build.
+ You can setup as many Runners as you need.
%br
- Runners can be placed on separate users, servers, and even on your local machine.
+ Runners can be placed on separate users, servers, even on your local machine.
%br
%div
- %span Each runner can be in one of the following states:
+ %span Each Runner can be in one of the following states:
%ul
%li
%span.label.label-success shared
- \- run builds from all unassigned projects
+ \- Runner runs builds from all unassigned projects
%li
%span.label.label-info specific
- \- run builds from assigned projects
+ \- Runner runs builds from assigned projects
%li
%span.label.label-warning locked
- \- runner cannot be assigned to other projects
+ \- Runner cannot be assigned to other projects
%li
%span.label.label-danger paused
- \- runner will not receive any new builds
+ \- Runner will not receive any new builds
.append-bottom-20.clearfix
.pull-left
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 61abfc6ecbe..a5e82e55cc1 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -11,14 +11,14 @@
- if @runner.shared?
.bs-callout.bs-callout-success
- %h4 This runner will process builds from ALL UNASSIGNED projects
+ %h4 This Runner will process builds from ALL UNASSIGNED projects
%p
- If you want runners to build only specific projects, enable them in the table below.
+ If you want Runners to build only specific projects, enable them in the table below.
Keep in mind that this is a one way transition.
- else
.bs-callout.bs-callout-info
- %h4 This runner will process builds only from ASSIGNED projects
- %p You can't make this a shared runner.
+ %h4 This Runner will process builds only from ASSIGNED projects
+ %p You can't make this a shared Runner.
%hr
.append-bottom-20
@@ -26,7 +26,7 @@
.row
.col-md-6
- %h4 Restrict projects for this runner
+ %h4 Restrict projects for this Runner
- if @runner.projects.any?
%table.table.assigned-projects
%thead
@@ -70,7 +70,7 @@
= paginate @projects
.col-md-6
- %h4 Recent builds served by this runner
+ %h4 Recent builds served by this Runner
%table.table.builds.runner-builds
%thead
%tr
diff --git a/app/views/ci/lints/_create.html.haml b/app/views/ci/lints/_create.html.haml
index f7875e68b7e..61c7cce20b2 100644
--- a/app/views/ci/lints/_create.html.haml
+++ b/app/views/ci/lints/_create.html.haml
@@ -16,18 +16,20 @@
%tr
%td #{stage.capitalize} Job - #{build[:name]}
%td
- %pre
- = simple_format build[:commands]
+ %pre= build[:commands]
%br
%b Tag list:
- = build[:tags]
+ = build[:tag_list].to_a.join(", ")
%br
%b Refs only:
- = build[:only] && build[:only].join(", ")
+ = @jobs[build[:name].to_sym][:only].to_a.join(", ")
%br
%b Refs except:
- = build[:except] && build[:except].join(", ")
+ = @jobs[build[:name].to_sym][:except].to_a.join(", ")
+ %br
+ %b Environment:
+ = build[:environment]
%br
%b When:
= build[:when]
diff --git a/app/views/dashboard/groups/_empty_state.html.haml b/app/views/dashboard/groups/_empty_state.html.haml
new file mode 100644
index 00000000000..f5222fe631e
--- /dev/null
+++ b/app/views/dashboard/groups/_empty_state.html.haml
@@ -0,0 +1,7 @@
+.groups-empty-state
+ = custom_icon("icon_empty_groups")
+
+ .text-content
+ %h4 A group is a collection of several projects.
+ %p If you organize your projects under a group, it works like a folder.
+ %p You can manage your group member’s permissions and access to each project in the group.
diff --git a/app/views/dashboard/groups/index.html.haml b/app/views/dashboard/groups/index.html.haml
index caca91af536..1a679c51774 100644
--- a/app/views/dashboard/groups/index.html.haml
+++ b/app/views/dashboard/groups/index.html.haml
@@ -2,9 +2,12 @@
- header_title "Groups", dashboard_groups_path
= render 'dashboard/groups_head'
-%ul.content-list
- - @group_members.each do |group_member|
- - group = group_member.group
- = render 'shared/groups/group', group: group, group_member: group_member
+- if @group_members.empty?
+ = render 'empty_state'
+- else
+ %ul.content-list
+ - @group_members.each do |group_member|
+ - group = group_member.group
+ = render 'shared/groups/group', group: group, group_member: group_member
-= paginate @group_members, theme: 'gitlab'
+ = paginate @group_members, theme: 'gitlab'
diff --git a/app/views/dashboard/snippets/index.html.haml b/app/views/dashboard/snippets/index.html.haml
index d4e7862981c..b2af438ea57 100644
--- a/app/views/dashboard/snippets/index.html.haml
+++ b/app/views/dashboard/snippets/index.html.haml
@@ -4,10 +4,10 @@
= render 'dashboard/snippets_head'
.nav-block
- .controls
- = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
+ .controls.hidden-xs
+ = link_to new_snippet_path, class: "btn btn-new", title: "New snippet" do
= icon('plus')
- New Snippet
+ New snippet
.nav-links.snippet-scope-menu
%li{ class: ("active" unless params[:scope]) }
@@ -34,5 +34,9 @@
%span.badge
= current_user.snippets.are_public.count
-= render 'snippets/snippets'
+ .visible-xs
+ = link_to new_snippet_path, class: "btn btn-new btn-block", title: "New snippet" do
+ = icon('plus')
+ New snippet
+= render 'snippets/snippets'
diff --git a/app/views/dashboard/todos/_todo.html.haml b/app/views/dashboard/todos/_todo.html.haml
index b40395c74de..cc077fad32a 100644
--- a/app/views/dashboard/todos/_todo.html.haml
+++ b/app/views/dashboard/todos/_todo.html.haml
@@ -19,6 +19,7 @@
(removed)
&middot; #{time_ago_with_tooltip(todo.created_at)}
+ = todo_due_date(todo)
.todo-body
.todo-note
diff --git a/app/views/dashboard/todos/index.html.haml b/app/views/dashboard/todos/index.html.haml
index 9d31f31c639..2a0302638ba 100644
--- a/app/views/dashboard/todos/index.html.haml
+++ b/app/views/dashboard/todos/index.html.haml
@@ -55,7 +55,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
= link_to todos_filter_path(sort: sort_value_priority) do
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 73c3a3dd2eb..20cd7b0179d 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -3,9 +3,9 @@
Almost there...
%p.lead
Please check your email to confirm your account
-- if after_sign_up_text.present?
+- if current_application_settings.after_sign_up_text.present?
.well-confirmation.text-center
- = markdown(after_sign_up_text)
+ = markdown_field(current_application_settings, :after_sign_up_text)
%p.confirmation-content.text-center
No confirmation email received? Please check your spam folder or
.append-bottom-20.prepend-top-20.text-center
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index 689cd6ed665..2ef383960f4 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -1,4 +1,4 @@
-= form_tag(user_omniauth_callback_path(server['provider_name']), id: 'new_ldap_user' ) do
+= form_tag(omniauth_callback_path(:user, server['provider_name']), id: 'new_ldap_user') do
= text_field_tag :username, nil, {class: "form-control top", placeholder: "#{server['label']} Login", autofocus: "autofocus"}
= password_field_tag :password, nil, {class: "form-control bottom", placeholder: "Password"}
- if devise_mapping.rememberable?
diff --git a/app/views/discussions/_resolve_all.html.haml b/app/views/discussions/_resolve_all.html.haml
index 7a8767ddba0..f0b61e0f7de 100644
--- a/app/views/discussions/_resolve_all.html.haml
+++ b/app/views/discussions/_resolve_all.html.haml
@@ -1,11 +1,10 @@
- if discussion.for_merge_request?
- %resolve-discussion-btn{ ":namespace-path" => "'#{discussion.project.namespace.path}'",
- ":project-path" => "'#{discussion.project.path}'",
+ %resolve-discussion-btn{ ":project-path" => "'#{project_path(discussion.project)}'",
":discussion-id" => "'#{discussion.id}'",
":merge-request-id" => discussion.noteable.iid,
":can-resolve" => discussion.can_resolve?(current_user),
"inline-template" => true }
.btn-group{ role: "group", "v-if" => "showButton" }
- %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading" }
+ %button.btn.btn-default{ type: "button", "@click" => "resolve", ":disabled" => "loading", "v-cloak" => "true" }
= icon("spinner spin", "v-show" => "loading")
{{ buttonText }}
diff --git a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
index bfa95ce79a7..9f02a8d2ed9 100644
--- a/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
+++ b/app/views/doorkeeper/authorized_applications/_delete_form.html.haml
@@ -6,4 +6,4 @@
= form_tag path do
%input{:name => "_method", :type => "hidden", :value => "delete"}/
- = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-link btn-remove btn-sm'
+ = submit_tag 'Revoke', onclick: "return confirm('Are you sure?')", class: 'btn btn-remove btn-sm'
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index b8248a80a27..a1b39d9e1a0 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -23,7 +23,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to explore_groups_path(sort: sort_value_recently_created) do
diff --git a/app/views/explore/projects/_filter.html.haml b/app/views/explore/projects/_filter.html.haml
index cd485da5104..4cff14b096b 100644
--- a/app/views/explore/projects/_filter.html.haml
+++ b/app/views/explore/projects/_filter.html.haml
@@ -7,8 +7,8 @@
= visibility_level_label(params[:visibility_level].to_i)
- else
Any
- %b.caret
- %ul.dropdown-menu
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(visibility_level: nil) do
Any
@@ -27,8 +27,8 @@
= params[:tag]
- else
Any
- %b.caret
- %ul.dropdown-menu
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_projects_path(tag: nil) do
Any
diff --git a/app/views/explore/snippets/index.html.haml b/app/views/explore/snippets/index.html.haml
index 6306fe6d0bf..7def9eacdc9 100644
--- a/app/views/explore/snippets/index.html.haml
+++ b/app/views/explore/snippets/index.html.haml
@@ -8,9 +8,8 @@
.row-content-block
- if current_user
- .pull-right
- = link_to new_snippet_path, class: "btn btn-new", title: "New Snippet" do
- New Snippet
+ = link_to new_snippet_path, class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
+ New snippet
.oneline
Public snippets created by you and other users are listed here
diff --git a/app/views/groups/milestones/new.html.haml b/app/views/groups/milestones/new.html.haml
index ca6c4326d1c..23d438b2aa1 100644
--- a/app/views/groups/milestones/new.html.haml
+++ b/app/views/groups/milestones/new.html.haml
@@ -33,8 +33,8 @@
.form-group
= f.label :projects, "Projects", class: "control-label"
.col-sm-10
- = f.collection_select :project_ids, @group.projects, :id, :name,
- { selected: @group.projects.map(&:id) }, multiple: true, class: 'select2'
+ = f.collection_select :project_ids, @group.projects.non_archived, :id, :name,
+ { selected: @group.projects.non_archived.pluck(:id) }, multiple: true, class: 'select2'
.col-md-6
.form-group
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 31db6ee0cad..fab61f447c2 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -21,7 +21,7 @@
- if @group.description.present?
.cover-desc.description
- = markdown(@group.description, pipeline: :description)
+ = markdown_field(@group, :description)
%div.groups-header{ class: container_class }
.top-area
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 57601ae9be0..31631887317 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -20,7 +20,7 @@
Read more about GitLab at #{link_to promo_host, promo_url, target: '_blank'}.
- if current_application_settings.help_page_text.present?
%hr
- = markdown(current_application_settings.help_page_text)
+ = markdown_field(current_application_settings, :help_page_text)
%hr
diff --git a/app/views/layouts/_flash.html.haml b/app/views/layouts/_flash.html.haml
index 3612f1ce5c6..baa8036de10 100644
--- a/app/views/layouts/_flash.html.haml
+++ b/app/views/layouts/_flash.html.haml
@@ -1,8 +1,10 @@
.flash-container.flash-container-page
- if alert
.flash-alert
- = alert
+ %div{ class: (container_class) }
+ %span= alert
- elsif notice
.flash-notice
- = notice
+ %div{ class: (container_class) }
+ %span= notice
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 4f7839a881f..8aefdcb3d9b 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -1,10 +1,11 @@
.page-with-sidebar{ class: "#{page_sidebar_class} #{page_gutter_class}" }
.sidebar-wrapper.nicescroll
.sidebar-action-buttons
- = link_to '#', class: 'nav-header-btn toggle-nav-collapse', title: "Open/Close" do
+ .nav-header-btn.toggle-nav-collapse{ title: "Open/Close" }
%span.sr-only Toggle navigation
= icon('bars')
- = link_to '#', class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: {placement: 'right', container: 'body'} do
+
+ %div{ class: "nav-header-btn pin-nav-btn has-tooltip #{'is-active' if pinned_nav?} js-nav-pin", title: pinned_nav? ? "Unpin navigation" : "Pin Navigation", data: { placement: 'right', container: 'body' } }
%span.sr-only Toggle navigation pinning
= icon('fw thumb-tack')
@@ -20,6 +21,7 @@
.container-fluid
= render "layouts/nav/#{nav}"
.content-wrapper{ class: "#{layout_nav_class}" }
+ = yield :sub_nav
= render "layouts/broadcast"
= render "layouts/flash"
= yield :flash_message
diff --git a/app/views/layouts/_search.html.haml b/app/views/layouts/_search.html.haml
index f7580f00159..d7386105b7d 100644
--- a/app/views/layouts/_search.html.haml
+++ b/app/views/layouts/_search.html.haml
@@ -2,15 +2,18 @@
- label = 'This group'
- if controller.controller_path =~ /^projects/ && @project.persisted?
- label = 'This project'
-
+- if @group && @group.persisted? && @group.path
+ - group_data_attrs = { group_path: j(@group.path), name: @group.name, issues_path: issues_group_path(j(@group.path)), mr_path: merge_requests_group_path(j(@group.path)) }
+- if @project && @project.persisted?
+ - project_data_attrs = { project_path: j(@project.path), name: j(@project.name), issues_path: namespace_project_issues_path(@project.namespace, @project), mr_path: namespace_project_merge_requests_path(@project.namespace, @project) }
.search.search-form{class: "#{'has-location-badge' if label.present?}"}
= form_tag search_path, method: :get, class: 'navbar-form' do |f|
.search-input-container
- if label.present?
.location-badge= label
.search-input-wrap
- .dropdown{ data: {url: search_autocomplete_path } }
- = search_field_tag "search", nil, placeholder: 'Search', class: "search-input dropdown-menu-toggle", spellcheck: false, tabindex: "1", autocomplete: 'off', data: { toggle: 'dropdown' }
+ .dropdown{ data: { url: search_autocomplete_path } }
+ = search_field_tag 'search', nil, placeholder: 'Search', class: 'search-input dropdown-menu-toggle js-search-dashboard-options', spellcheck: false, tabindex: '1', autocomplete: 'off', data: { toggle: 'dropdown', issues_path: issues_dashboard_url, mr_path: merge_requests_dashboard_url }
.dropdown-menu.dropdown-select
= dropdown_content do
%ul
@@ -21,8 +24,9 @@
%i.search-icon
%i.clear-icon.js-clear-input
- = hidden_field_tag :group_id, @group.try(:id)
- = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id'
+ = hidden_field_tag :group_id, @group.try(:id), class: 'js-search-group-options', data: group_data_attrs
+
+ = hidden_field_tag :project_id, @project && @project.persisted? ? @project.id : '', id: 'search_project_id', class: 'js-search-project-options', data: project_data_attrs
- if @project && @project.persisted?
- if current_controller?(:issues)
@@ -36,31 +40,6 @@
- else
= hidden_field_tag :search_code, true
- :javascript
- gl.projectOptions = gl.projectOptions || {};
- gl.projectOptions["#{j(@project.path)}"] = {
- issuesPath: "#{namespace_project_issues_path(@project.namespace, @project)}",
- mrPath: "#{namespace_project_merge_requests_path(@project.namespace, @project)}",
- name: "#{j(@project.name)}"
- };
-
- - if @group && @group.persisted? && @group.path
- :javascript
- gl.groupOptions = gl.groupOptions || {};
- gl.groupOptions["#{j(@group.path)}"] = {
- name: "#{j(@group.name)}",
- issuesPath: "#{issues_group_path(j(@group.path))}",
- mrPath: "#{merge_requests_group_path(j(@group.path))}"
- };
-
-
- :javascript
- gl.dashboardOptions = {
- issuesPath: "#{issues_dashboard_url}",
- mrPath: "#{merge_requests_dashboard_url}"
- };
-
-
- if @snippet || @snippets
= hidden_field_tag :snippets, true
= hidden_field_tag :repository_ref, @ref
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 3d28eec84ef..a9a384bd5f3 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -25,8 +25,8 @@
Perform code reviews and enhance collaboration with merge requests.
Each project can also have an issue tracker and a wiki.
- - if extra_sign_in_text.present?
- = markdown(extra_sign_in_text)
+ - if current_application_settings.sign_in_text.present?
+ = markdown_field(current_application_settings, :sign_in_text)
%hr
.container
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 94c53882623..7faa8bded86 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -1,5 +1,5 @@
%header.navbar.navbar-fixed-top.navbar-gitlab{ class: nav_header_class }
- %div{ class: fluid_layout ? "container-fluid" : "container-fluid" }
+ %div{ class: "container-fluid" }
.header-content
%button.side-nav-toggle{ type: 'button', "aria-label" => "Toggle global navigation" }
%span.sr-only Toggle navigation
@@ -41,7 +41,7 @@
%li.header-user.dropdown
= link_to current_user, class: "header-user-dropdown-toggle", data: { toggle: "dropdown" } do
= image_tag avatar_icon(current_user, 26), width: 26, height: 26, class: "header-user-avatar"
- %span.caret
+ = icon('caret-down')
.dropdown-menu-nav.dropdown-menu-align-right
%ul
%li
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 4433cab7782..8966dd3fd86 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -4,9 +4,9 @@ $('body').addClass('<%= user_application_theme %>')
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
- $('.content-wrapper').find('.container-fluid').removeClass('container-limited')
+ $('.content-wrapper .container-fluid').removeClass('container-limited')
} else {
- $('.content-wrapper').find('.container-fluid').addClass('container-limited')
+ $('.content-wrapper .container-fluid').addClass('container-limited')
}
// Re-enable the "Save" button
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index d9fa74fad90..578af9fe98d 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -87,6 +87,9 @@
= f.label :location, 'Location', class: "label-light"
= f.text_field :location, class: "form-control"
.form-group
+ = f.label :organization, 'Organization', class: "label-light"
+ = f.text_field :organization, class: "form-control"
+ .form-group
= f.label :bio, class: "label-light"
= f.text_area :bio, rows: 4, class: "form-control", maxlength: 250
%span.help-block Tell us about yourself in fewer than 250 characters.
diff --git a/app/views/projects/_activity.html.haml b/app/views/projects/_activity.html.haml
index ac50ce83f6a..d011e51e696 100644
--- a/app/views/projects/_activity.html.haml
+++ b/app/views/projects/_activity.html.haml
@@ -1,13 +1,16 @@
-.nav-block.activity-filter-block
- - if current_user
- .controls
- = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
- %i.fa.fa-rss
+- @no_container = true
- = render 'shared/event_filter'
+%div{ class: container_class }
+ .nav-block.activity-filter-block
+ - if current_user
+ .controls
+ = link_to namespace_project_path(@project.namespace, @project, format: :atom, private_token: current_user.private_token), title: "Feed", class: 'btn rss-btn' do
+ = icon('rss')
-.content_list.project-activity{:"data-href" => activity_project_path(@project)}
-= spinner
+ = render 'shared/event_filter'
+
+ .content_list.project-activity{:"data-href" => activity_project_path(@project)}
+ = spinner
:javascript
var activity = new Activities();
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 8ef31ca3bda..5590198a20e 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -9,7 +9,7 @@
.project-home-desc
- if @project.description.present?
- = markdown(@project.description, pipeline: :description)
+ = markdown_field(@project, :description)
- if forked_from_project = @project.forked_from_project
%p
diff --git a/app/views/projects/_last_push.html.haml b/app/views/projects/_last_push.html.haml
index 3c6b931f41a..1c3bccccb5c 100644
--- a/app/views/projects/_last_push.html.haml
+++ b/app/views/projects/_last_push.html.haml
@@ -1,6 +1,6 @@
- if event = last_push_event
- if show_last_push_widget?(event)
- .row-content-block.top-block.clear-block.hidden-xs
+ .row-content-block.top-block.hidden-xs.white
%div{ class: container_class }
.event-last-push
.event-last-push-text
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index 0237e152b54..d4f59764a70 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -21,6 +21,13 @@
= dropdown_tag("Choose a .gitignore template", options: { toggle_class: 'js-gitignore-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitignore_names } } )
.gitlab-ci-yml-selector.js-gitlab-ci-yml-selector-wrap.hidden
= dropdown_tag("Choose a GitLab CI Yaml template", options: { toggle_class: 'js-gitlab-ci-yml-selector', title: "Choose a template", filter: true, placeholder: "Filter", data: { data: gitlab_ci_ymls } } )
+ = button_tag class: 'soft-wrap-toggle btn', type: 'button' do
+ %span.no-wrap
+ = custom_icon('icon_no_wrap')
+ No wrap
+ %span.soft-wrap
+ = custom_icon('icon_soft_wrap')
+ Soft wrap
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/boards/components/_board.html.haml b/app/views/projects/boards/components/_board.html.haml
index 73066150fb3..ba1502c97b6 100644
--- a/app/views/projects/boards/components/_board.html.haml
+++ b/app/views/projects/boards/components/_board.html.haml
@@ -12,8 +12,17 @@
%header.board-header{ ":class" => "{ 'has-border': list.label }", ":style" => "{ borderTopColor: (list.label ? list.label.color : null) }" }
%h3.board-title.js-board-handle{ ":class" => "{ 'user-can-drag': (!disabled && !list.preset) }" }
{{ list.title }}
- %span.pull-right{ "v-if" => "list.type !== 'blank'" }
- {{ list.issuesSize }}
+ .board-issue-count-holder.pull-right.clearfix{ "v-if" => "list.type !== 'blank'" }
+ %span.board-issue-count.pull-left{ ":class" => "{ 'has-btn': list.type !== 'done' }" }
+ {{ list.issuesSize }}
+ - if can?(current_user, :admin_issue, @project)
+ %button.btn.btn-small.btn-default.pull-right.has-tooltip{ type: "button",
+ "@click" => "showNewIssueForm",
+ "v-if" => "list.type !== 'done'",
+ "aria-label" => "Add an issue",
+ "title" => "Add an issue",
+ data: { placement: "top", container: "body" } }
+ = icon("plus")
- if can?(current_user, :admin_list, @project)
%board-delete{ "inline-template" => true,
":list" => "list",
@@ -26,12 +35,38 @@
":issues" => "list.issues",
":loading" => "list.loading",
":disabled" => "disabled",
+ ":show-issue-form.sync" => "showIssueForm",
":issue-link-base" => "issueLinkBase" }
.board-list-loading.text-center{ "v-if" => "loading" }
= icon("spinner spin")
+ - if can? current_user, :create_issue, @project
+ %board-new-issue{ "inline-template" => true,
+ ":list" => "list",
+ ":show-issue-form.sync" => "showIssueForm",
+ "v-show" => "list.type !== 'done' && showIssueForm" }
+ .card.board-new-issue-form
+ %form{ "@submit" => "submit($event)" }
+ .flash-container{ "v-if" => "error" }
+ .flash-alert
+ An error occured. Please try again.
+ %label.label-light{ ":for" => "list.id + '-title'" }
+ Title
+ %input.form-control{ type: "text",
+ "v-model" => "title",
+ "v-el:input" => true,
+ ":id" => "list.id + '-title'" }
+ .clearfix.prepend-top-10
+ %button.btn.btn-success.pull-left{ type: "submit",
+ ":disabled" => "title === ''",
+ "v-el:submit-button" => true }
+ Submit issue
+ %button.btn.btn-default.pull-right{ type: "button",
+ "@click" => "cancel" }
+ Cancel
%ul.board-list{ "v-el:list" => true,
"v-show" => "!loading",
- ":data-board" => "list.id" }
+ ":data-board" => "list.id",
+ ":class" => "{ 'is-smaller': showIssueForm }" }
= render "projects/boards/components/card"
%li.board-list-count.text-center{ "v-if" => "showCount" }
= icon("spinner spin", "v-show" => "list.loadingMore" )
diff --git a/app/views/projects/boards/components/_card.html.haml b/app/views/projects/boards/components/_card.html.haml
index e8b60b54d80..d8f16022407 100644
--- a/app/views/projects/boards/components/_card.html.haml
+++ b/app/views/projects/boards/components/_card.html.haml
@@ -7,7 +7,7 @@
":issue-link-base" => "issueLinkBase",
":disabled" => "disabled",
"track-by" => "id" }
- %li.card{ ":class" => "{ 'user-can-drag': !disabled }",
+ %li.card{ ":class" => "{ 'user-can-drag': !disabled && issue.id, 'is-disabled': disabled || !issue.id }",
":index" => "index" }
%h4.card-title
= icon("eye-slash", class: "confidential-icon", "v-if" => "issue.confidential")
@@ -15,7 +15,7 @@
":title" => "issue.title" }
{{ issue.title }}
.card-footer
- %span.card-number
+ %span.card-number{ "v-if" => "issue.id" }
= precede '#' do
{{ issue.id }}
%button.label.color-label.has-tooltip{ "v-for" => "label in issue.labels",
@@ -26,7 +26,7 @@
":title" => "label.description",
data: { container: 'body' } }
{{ label.title }}
- %a.has-tooltip{ ":href" => "'/u/' + issue.assignee.username",
+ %a.has-tooltip{ ":href" => "'/' + issue.assignee.username",
":title" => "'Assigned to ' + issue.assignee.name",
"v-if" => "issue.assignee",
data: { container: 'body' } }
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index e889f29c816..84f38575e84 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -15,7 +15,7 @@
%button.dropdown-toggle.btn{type: 'button', 'data-toggle' => 'dropdown'}
%span.light
= projects_sort_options_hash[@sort]
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_branches_path(sort: sort_value_name) do
diff --git a/app/views/projects/builds/_sidebar.html.haml b/app/views/projects/builds/_sidebar.html.haml
index 0aa3092baa2..966633f1f89 100644
--- a/app/views/projects/builds/_sidebar.html.haml
+++ b/app/views/projects/builds/_sidebar.html.haml
@@ -8,7 +8,7 @@
%a.gutter-toggle.pull-right.js-sidebar-build-toggle{ href: "#" }
= icon('angle-double-right')
- if @build.coverage
- .block.block-first
+ .block.coverage
.title
Test coverage
%p.build-detail-row
@@ -95,7 +95,7 @@
- @build.trigger_request.variables.each do |key, value|
.hide.js-build
- .js-build-variable= key
+ .js-build-variable= key
.js-build-value= value
.block
@@ -128,7 +128,7 @@
- builds.select{|build| build.status == build_status}.each do |build|
.build-job{class: ('active' if build == @build), data: {stage: build.stage}}
= link_to namespace_project_build_path(@project.namespace, @project, build) do
- = icon('check')
+ = icon('arrow-right')
= ci_icon_for_status(build.status)
%span
- if build.name
diff --git a/app/views/projects/builds/_table.html.haml b/app/views/projects/builds/_table.html.haml
index c2bcfb773a6..f3747ba2a21 100644
--- a/app/views/projects/builds/_table.html.haml
+++ b/app/views/projects/builds/_table.html.haml
@@ -1,7 +1,7 @@
- admin = local_assigns.fetch(:admin, false)
- if builds.blank?
- %li
+ %div
.nothing-here-block No builds to show
- else
.table-holder
diff --git a/app/views/projects/builds/index.html.haml b/app/views/projects/builds/index.html.haml
index 5c60b7a7364..06070f12bbd 100644
--- a/app/views/projects/builds/index.html.haml
+++ b/app/views/projects/builds/index.html.haml
@@ -19,5 +19,5 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %ul.content-list.builds-content-list
+ %div.content-list.builds-content-list
= render "table", builds: @builds, project: @project
diff --git a/app/views/projects/buttons/_download.html.haml b/app/views/projects/buttons/_download.html.haml
index 24de020917a..9089586a89d 100644
--- a/app/views/projects/buttons/_download.html.haml
+++ b/app/views/projects/buttons/_download.html.haml
@@ -1,9 +1,9 @@
- if !project.empty_repo? && can?(current_user, :download_code, project)
- %span.btn-group{class: 'hidden-xs hidden-sm btn-grouped'}
+ %span{class: 'hidden-xs hidden-sm'}
.dropdown.inline
%button.btn{ 'data-toggle' => 'dropdown' }
= icon('download')
- %span.caret
+ = icon("caret-down")
%span.sr-only
Select Archive Format
%ul.dropdown-menu.dropdown-menu-align-right{ role: 'menu' }
diff --git a/app/views/projects/buttons/_dropdown.html.haml b/app/views/projects/buttons/_dropdown.html.haml
index ca907077c2b..6cd9b98a706 100644
--- a/app/views/projects/buttons/_dropdown.html.haml
+++ b/app/views/projects/buttons/_dropdown.html.haml
@@ -1,7 +1,8 @@
- if current_user
- .btn-group
+ .dropdown.inline.project-dropdown
%a.btn.dropdown-toggle{href: '#', "data-toggle" => "dropdown"}
= icon('plus')
+ = icon("caret-down")
%ul.dropdown-menu.dropdown-menu-align-right.project-home-dropdown
- can_create_issue = can?(current_user, :create_issue, @project)
- merge_project = can?(current_user, :create_merge_request, @project) ? @project : (current_user && current_user.fork_of(@project))
diff --git a/app/views/projects/buttons/_fork.html.haml b/app/views/projects/buttons/_fork.html.haml
index 22db33498f1..29d549a60f5 100644
--- a/app/views/projects/buttons/_fork.html.haml
+++ b/app/views/projects/buttons/_fork.html.haml
@@ -5,10 +5,10 @@
= custom_icon('icon_fork')
%span Fork
- else
- = link_to new_namespace_project_fork_path(@project.namespace, @project), title: "Fork project", class: 'btn has-tooltip' do
+ = link_to new_namespace_project_fork_path(@project.namespace, @project), title: 'Fork project', class: 'btn has-tooltip' do
= custom_icon('icon_fork')
%span Fork
%div.count-with-arrow
%span.arrow
- = link_to namespace_project_forks_path(@project.namespace, @project), class: "count" do
+ = link_to namespace_project_forks_path(@project.namespace, @project), title: 'Forks', class: 'count has-tooltip' do
= @project.forks_count
diff --git a/app/views/projects/ci/builds/_build.html.haml b/app/views/projects/ci/builds/_build.html.haml
index 75192c48188..9248adfde80 100644
--- a/app/views/projects/ci/builds/_build.html.haml
+++ b/app/views/projects/ci/builds/_build.html.haml
@@ -13,45 +13,44 @@
- else
= ci_status_with_icon(build.status)
- %td
- .branch-commit
- - if can?(current_user, :read_build, build)
- = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
- %span.build-link ##{build.id}
- - else
+ %td.branch-commit
+ - if can?(current_user, :read_build, build)
+ = link_to namespace_project_build_url(build.project.namespace, build.project, build) do
%span.build-link ##{build.id}
+ - else
+ %span.build-link ##{build.id}
- - if ref
- - if build.ref
- .icon-container
- = build.tag? ? icon('tag') : icon('code-fork')
- = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
- - else
- .light none
+ - if ref
+ - if build.ref
.icon-container
- = custom_icon("icon_commit")
+ = build.tag? ? icon('tag') : icon('code-fork')
+ = link_to build.ref, namespace_project_commits_path(build.project.namespace, build.project, build.ref), class: "monospace branch-name"
+ - else
+ .light none
+ .icon-container
+ = custom_icon("icon_commit")
- - if commit_sha
- = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
+ - if commit_sha
+ = link_to build.short_sha, namespace_project_commit_path(build.project.namespace, build.project, build.sha), class: "commit-id monospace"
- - if build.stuck?
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
- - if retried
- = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
+ - if build.stuck?
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Build is stuck. Check runners.')
+ - if retried
+ = icon('warning', class: 'text-warning has-tooltip', title: 'Build was retried.')
- .label-container
- - if build.tags.any?
- - build.tags.each do |tag|
- %span.label.label-primary
- = tag
- - if build.try(:trigger_request)
- %span.label.label-info triggered
- - if build.try(:allow_failure)
- %span.label.label-danger allowed to fail
- - if retried
- %span.label.label-warning retried
- - if build.manual?
- %span.label.label-info manual
+ .label-container
+ - if build.tags.any?
+ - build.tags.each do |tag|
+ %span.label.label-primary
+ = tag
+ - if build.try(:trigger_request)
+ %span.label.label-info triggered
+ - if build.try(:allow_failure)
+ %span.label.label-danger allowed to fail
+ - if retried
+ %span.label.label-warning retried
+ - if build.manual?
+ %span.label.label-info manual
- if admin
%td
diff --git a/app/views/projects/ci/builds/_build_pipeline.html.haml b/app/views/projects/ci/builds/_build_pipeline.html.haml
index 547bc0c9c19..017d3ff6af2 100644
--- a/app/views/projects/ci/builds/_build_pipeline.html.haml
+++ b/app/views/projects/ci/builds/_build_pipeline.html.haml
@@ -5,8 +5,10 @@
.ci-status-text= subject.name
- elsif can?(current_user, :read_build, @project)
= link_to namespace_project_build_path(subject.project.namespace, subject.project, subject) do
- = render_status_with_link('build', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('build', subject.status)
.ci-status-text= subject.name
- else
- = render_status_with_link('build', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('build', subject.status)
= ci_icon_for_status(subject.status)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index 6391c67021b..36eadbd2bf1 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -1,4 +1,7 @@
- status = pipeline.status
+- show_commit = local_assigns.fetch(:show_commit, true)
+- show_branch = local_assigns.fetch(:show_branch, true)
+
%tr.commit
%td.commit-link
= link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
@@ -6,33 +9,32 @@
= ci_icon_for_status(status)
- else
= ci_status_with_icon(status)
- %td
- .branch-commit
- = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
- %span ##{pipeline.id}
- - if pipeline.ref
- - unless defined?(hide_branch) && hide_branch
- .icon-container
- = pipeline.tag? ? icon('tag') : icon('code-fork')
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
+ %td.branch-commit
+ = link_to namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id) do
+ %span ##{pipeline.id}
+ - if pipeline.ref && show_branch
+ .icon-container
+ = pipeline.tag? ? icon('tag') : icon('code-fork')
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace branch-name"
+ - if show_commit
.icon-container
= custom_icon("icon_commit")
= link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "commit-id monospace"
- - if pipeline.latest?
- %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
- - if pipeline.triggered?
- %span.label.label-primary triggered
- - if pipeline.yaml_errors.present?
- %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
- - if pipeline.builds.any?(&:stuck?)
- %span.label.label-warning stuck
+ - if pipeline.latest?
+ %span.label.label-success.has-tooltip{ title: 'Latest build for this branch' } latest
+ - if pipeline.triggered?
+ %span.label.label-primary triggered
+ - if pipeline.yaml_errors.present?
+ %span.label.label-danger.has-tooltip{ title: "#{pipeline.yaml_errors}" } yaml invalid
+ - if pipeline.builds.any?(&:stuck?)
+ %span.label.label-warning stuck
- %p.commit-title
- - if commit = pipeline.commit
- = author_avatar(commit, size: 20)
- = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
- - else
- Cant find HEAD commit for this branch
+ %p.commit-title
+ - if commit = pipeline.commit
+ = author_avatar(commit, size: 20)
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(pipeline.project.namespace, pipeline.project, commit.id), class: "commit-row-message"
+ - else
+ Cant find HEAD commit for this branch
- stages_status = pipeline.statuses.relevant.latest.stages_status
@@ -55,8 +57,8 @@
= icon("calendar")
#{time_ago_with_tooltip(pipeline.finished_at, short_format: false, skip_js: true)}
- %td.pipeline-actions
- .controls.hidden-xs.pull-right
+ %td.pipeline-actions.hidden-xs
+ .controls.pull-right
- artifacts = pipeline.builds.latest.with_artifacts_not_expired
- actions = pipeline.manual_actions
- if artifacts.present? || actions.any?
@@ -65,7 +67,7 @@
.btn-group
%a.dropdown-toggle.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |build|
%li
@@ -76,7 +78,7 @@
.btn-group
%a.dropdown-toggle.btn.btn-default.build-artifacts{type: 'button', 'data-toggle' => 'dropdown'}
= icon("download")
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- artifacts.each do |build|
%li
diff --git a/app/views/projects/commit/_builds.html.haml b/app/views/projects/commit/_builds.html.haml
index a508382578a..b7087749428 100644
--- a/app/views/projects/commit/_builds.html.haml
+++ b/app/views/projects/commit/_builds.html.haml
@@ -1,2 +1,2 @@
-- @pipelines.each do |pipeline|
+- @ci_pipelines.each do |pipeline|
= render "pipeline", pipeline: pipeline, pipeline_details: true
diff --git a/app/views/projects/commit/_ci_menu.html.haml b/app/views/projects/commit/_ci_menu.html.haml
index 935433306ea..cbfd99ca448 100644
--- a/app/views/projects/commit/_ci_menu.html.haml
+++ b/app/views/projects/commit/_ci_menu.html.haml
@@ -3,6 +3,11 @@
= link_to namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Changes
%span.badge= @diffs.size
+ - if can?(current_user, :read_pipeline, @project)
+ = nav_link(path: 'commit#pipelines') do
+ = link_to pipelines_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
+ Pipelines
+ %span.badge= @ci_pipelines.count
= nav_link(path: 'commit#builds') do
= link_to builds_namespace_project_commit_path(@project.namespace, @project, @commit.id) do
Builds
diff --git a/app/views/projects/commit/_commit_box.html.haml b/app/views/projects/commit/_commit_box.html.haml
index 29d767e7769..6c82a4e5600 100644
--- a/app/views/projects/commit/_commit_box.html.haml
+++ b/app/views/projects/commit/_commit_box.html.haml
@@ -14,7 +14,7 @@
.dropdown.inline
%a.btn.btn-default.dropdown-toggle{ data: { toggle: "dropdown" } }
%span.hidden-xs Options
- %span.caret.commit-options-dropdown-caret
+ = icon('caret-down', class: ".commit-options-dropdown-caret")
%ul.dropdown-menu.dropdown-menu-align-right
%li.visible-xs-block.visible-sm-block
= link_to namespace_project_tree_path(@project.namespace, @project, @commit) do
@@ -24,6 +24,8 @@
= revert_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
%li.clearfix
= cherry_pick_commit_link(@commit, namespace_project_commit_path(@project.namespace, @project, @commit.id), has_tooltip: false)
+ %li.clearfix
+ = link_to "Tag", new_namespace_project_tag_path(@project.namespace, @project, ref: @commit)
%li.divider
%li.dropdown-header
Download
@@ -63,10 +65,10 @@
.commit-box.content-block
%h3.commit-title
- = markdown escape_once(@commit.title), pipeline: :single_line, author: @commit.author
+ = markdown(@commit.title, pipeline: :single_line, author: @commit.author)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(escape_once(@commit.description), pipeline: :single_line, author: @commit.author))
+ = preserve(markdown(@commit.description, pipeline: :single_line, author: @commit.author))
:javascript
$(".commit-info.branches").load("#{branches_namespace_project_commit_path(@project.namespace, @project, @commit.id)}");
diff --git a/app/views/projects/commit/_pipeline.html.haml b/app/views/projects/commit/_pipeline.html.haml
index 9258f4b3c25..288c06d9b67 100644
--- a/app/views/projects/commit/_pipeline.html.haml
+++ b/app/views/projects/commit/_pipeline.html.haml
@@ -1,45 +1,46 @@
-.row-content-block.build-content.middle-block.pipeline-actions
- .pull-right
- .btn.btn-grouped.btn-white.toggle-pipeline-btn
- %span.toggle-btn-text Hide
- %span pipeline graph
- %span.caret
- - if can?(current_user, :update_pipeline, pipeline.project)
- - if pipeline.builds.latest.failed.any?(&:retryable?)
- = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
+.pipeline-graph-container
+ .row-content-block.build-content.middle-block.pipeline-actions
+ .pull-right
+ .btn.btn-grouped.btn-white.toggle-pipeline-btn
+ %span.toggle-btn-text Hide
+ %span pipeline graph
+ %span.caret
+ - if can?(current_user, :update_pipeline, pipeline.project)
+ - if pipeline.builds.latest.failed.any?(&:retryable?)
+ = link_to "Retry failed", retry_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: 'btn btn-grouped btn-primary', method: :post
- - if pipeline.builds.running_or_pending.any?
- = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
+ - if pipeline.builds.running_or_pending.any?
+ = link_to "Cancel running", cancel_namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-grouped btn-danger', method: :post
- .oneline.clearfix
- - if defined?(pipeline_details) && pipeline_details
- Pipeline
- = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
- with
- = pluralize pipeline.statuses.count(:id), "build"
- - if pipeline.ref
- for
- = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
- - if defined?(link_to_commit) && link_to_commit
- for commit
- = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
- - if pipeline.duration
- in
- = time_interval_in_words pipeline.duration
+ .oneline.clearfix
+ - if defined?(pipeline_details) && pipeline_details
+ Pipeline
+ = link_to "##{pipeline.id}", namespace_project_pipeline_path(pipeline.project.namespace, pipeline.project, pipeline.id), class: "monospace"
+ with
+ = pluralize pipeline.statuses.count(:id), "build"
+ - if pipeline.ref
+ for
+ = link_to pipeline.ref, namespace_project_commits_path(pipeline.project.namespace, pipeline.project, pipeline.ref), class: "monospace"
+ - if defined?(link_to_commit) && link_to_commit
+ for commit
+ = link_to pipeline.short_sha, namespace_project_commit_path(pipeline.project.namespace, pipeline.project, pipeline.sha), class: "monospace"
+ - if pipeline.duration
+ in
+ = time_interval_in_words pipeline.duration
-.row-content-block.build-content.middle-block.pipeline-graph
- .pipeline-visualization
- %ul.stage-column-list
- - stages = pipeline.stages_with_latest_statuses
- - stages.each do |stage, statuses|
- %li.stage-column
- .stage-name
- %a{name: stage}
- - if stage
- = stage.titleize
- .builds-container
- %ul
- = render "projects/commit/pipeline_stage", statuses: statuses
+ .row-content-block.build-content.middle-block.pipeline-graph.hidden
+ .pipeline-visualization
+ %ul.stage-column-list
+ - stages = pipeline.stages_with_latest_statuses
+ - stages.each do |stage, statuses|
+ %li.stage-column
+ .stage-name
+ %a{name: stage}
+ - if stage
+ = stage.titleize
+ .builds-container
+ %ul
+ = render "projects/commit/pipeline_stage", statuses: statuses
- if pipeline.yaml_errors.present?
diff --git a/app/views/projects/commit/_pipeline_stage.html.haml b/app/views/projects/commit/_pipeline_stage.html.haml
index 23c5c51fbc2..289aa5178b1 100644
--- a/app/views/projects/commit/_pipeline_stage.html.haml
+++ b/app/views/projects/commit/_pipeline_stage.html.haml
@@ -10,5 +10,5 @@
- else
%li.build
.curve
- .build-content
+ .dropdown.inline.build-content
= render "projects/commit/pipeline_status_group", name: group_name, subject: grouped_statuses
diff --git a/app/views/projects/commit/_pipeline_status_group.html.haml b/app/views/projects/commit/_pipeline_status_group.html.haml
index 4e7a6f1af08..6ada719e006 100644
--- a/app/views/projects/commit/_pipeline_status_group.html.haml
+++ b/app/views/projects/commit/_pipeline_status_group.html.haml
@@ -1,11 +1,12 @@
- group_status = CommitStatus.where(id: subject).status
-= render_status_with_link('build', group_status)
-.dropdown.inline
- %button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
- %span.ci-status-text
- = name
- %span.badge= subject.size
- %ul.dropdown-menu.grouped-pipeline-dropdown
- .arrow
- - subject.each do |status|
+%button.dropdown-menu-toggle{ type: 'button', data: { toggle: 'dropdown' } }
+ %span.ci-status-icon
+ = render_status_with_link('build', group_status)
+ %span.ci-status-text
+ = name
+ %span.badge= subject.size
+%ul.dropdown-menu.grouped-pipeline-dropdown
+ %li.arrow
+ - subject.each do |status|
+ %li
= render "projects/#{status.to_partial_path}_pipeline", subject: status
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index f41a11a056d..998812793a2 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -7,14 +7,8 @@
%table.table.builds
%tbody
%th Status
- %th Commit
- - pipelines.stages.each do |stage|
- %th.stage
- - if stage.titleize.length > 12
- %span.has-tooltip{ title: "#{stage.titleize}" }
- = stage.titleize
- - else
- = stage.titleize
+ %th Pipeline
+ %th Stages
%th
%th
- = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, hide_branch: true
+ = render pipelines, commit_sha: true, stage: true, allow_retry: true, stages: pipelines.stages, status_icon_only: true, show_commit: false
diff --git a/app/views/projects/commit/pipelines.html.haml b/app/views/projects/commit/pipelines.html.haml
new file mode 100644
index 00000000000..d85d6729a81
--- /dev/null
+++ b/app/views/projects/commit/pipelines.html.haml
@@ -0,0 +1,7 @@
+- page_title "Pipelines", "#{@commit.title} (#{@commit.short_id})", "Commits"
+
+.prepend-top-default
+ = render "commit_box"
+
+= render "ci_menu"
+= render "pipelines_list", pipelines: @ci_pipelines
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index 389477d0927..fb48aef0559 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -33,7 +33,7 @@
- if commit.description?
%pre.commit-row-description.js-toggle-content
- = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
+ = preserve(markdown(commit.description, pipeline: :single_line, author: commit.author))
.commit-row-info
= commit_author_link(commit, avatar: false, size: 24)
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index 4d1ee1c5318..80763ce67ca 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,27 +1,28 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
- = link_to project_files_path(@project) do
- Files
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
+ = link_to project_files_path(@project) do
+ Files
- = nav_link(controller: [:commit, :commits]) do
- = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
- Commits
+ = nav_link(controller: [:commit, :commits]) do
+ = link_to namespace_project_commits_path(@project.namespace, @project, current_ref) do
+ Commits
- = nav_link(controller: %w(network)) do
- = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
- Network
+ = nav_link(controller: %w(network)) do
+ = link_to namespace_project_network_path(@project.namespace, @project, current_ref) do
+ Network
- = nav_link(controller: :compare) do
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
- Compare
+ = nav_link(controller: :compare) do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: current_ref) do
+ Compare
- = nav_link(html_options: {class: branches_tab_class}) do
- = link_to namespace_project_branches_path(@project.namespace, @project) do
- Branches
+ = nav_link(html_options: {class: branches_tab_class}) do
+ = link_to namespace_project_branches_path(@project.namespace, @project) do
+ Branches
- = nav_link(controller: [:tags, :releases]) do
- = link_to namespace_project_tags_path(@project.namespace, @project) do
- Tags
+ = nav_link(controller: [:tags, :releases]) do
+ = link_to namespace_project_tags_path(@project.namespace, @project) do
+ Tags
diff --git a/app/views/projects/commits/show.html.haml b/app/views/projects/commits/show.html.haml
index 9a44ba94970..876c8002627 100644
--- a/app/views/projects/commits/show.html.haml
+++ b/app/views/projects/commits/show.html.haml
@@ -5,7 +5,8 @@
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-= render "head"
+= content_for :sub_nav do
+ = render "head"
%div{ class: container_class }
.row-content-block.second-block.content-component-block
diff --git a/app/views/projects/compare/_form.html.haml b/app/views/projects/compare/_form.html.haml
index d79336f5a60..76b68c544aa 100644
--- a/app/views/projects/compare/_form.html.haml
+++ b/app/views/projects/compare/_form.html.haml
@@ -1,17 +1,22 @@
= form_tag namespace_project_compare_index_path(@project.namespace, @project), method: :post, class: 'form-inline js-requires-input' do
.clearfix
- if params[:to] && params[:from]
- = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
- .form-group.dropdown.compare-form-group.js-compare-from-dropdown
+ .compare-switch-container
+ = link_to icon('exchange'), {from: params[:to], to: params[:from]}, {class: 'commits-compare-switch has-tooltip', title: 'Switch base of comparison'}
+ .form-group.dropdown.compare-form-group.from.js-compare-from-dropdown
.input-group.inline-input-group
%span.input-group-addon from
- = text_field_tag :from, params[:from], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from].presence }
+ = hidden_field_tag :from, params[:from]
+ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-from-dropdown", selected: params[:from], field_name: :from } do
+ .dropdown-toggle-text= params[:from] || 'Select branch/tag'
= render "ref_dropdown"
- = "..."
- .form-group.dropdown.compare-form-group.js-compare-to-dropdown
+ .compare-ellipsis ...
+ .form-group.dropdown.compare-form-group.to.js-compare-to-dropdown
.input-group.inline-input-group
%span.input-group-addon to
- = text_field_tag :to, params[:to], class: "form-control js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to].presence }
+ = hidden_field_tag :to, params[:to]
+ = button_tag type: 'button', class: "form-control compare-dropdown-toggle js-compare-dropdown", required: true, data: { refs_url: refs_namespace_project_path(@project.namespace, @project), toggle: "dropdown", target: ".js-compare-to-dropdown", selected: params[:to], field_name: :to } do
+ .dropdown-toggle-text= params[:to] || 'Select branch/tag'
= render "ref_dropdown"
&nbsp;
= button_tag "Compare", class: "btn btn-create commits-compare-btn"
diff --git a/app/views/projects/compare/_ref_dropdown.html.haml b/app/views/projects/compare/_ref_dropdown.html.haml
index c604c6d0135..27d928c87a0 100644
--- a/app/views/projects/compare/_ref_dropdown.html.haml
+++ b/app/views/projects/compare/_ref_dropdown.html.haml
@@ -1,4 +1,5 @@
.dropdown-menu.dropdown-menu-selectable
= dropdown_title "Select branch/tag"
+ = dropdown_filter "Filter by branch/tag"
= dropdown_content
= dropdown_loading
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index e9ff8e90dd5..45be6581cfc 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -4,7 +4,7 @@
%div{ class: container_class }
.sub-header-block
- Compare branches, tags or commit ranges.
+ Compare Git revisions.
%br
Fill input field with commit id like
%code.label-branch 4eedf23
diff --git a/app/views/projects/deployments/_actions.haml b/app/views/projects/deployments/_actions.haml
index 16d134eb6b6..22c4a75d213 100644
--- a/app/views/projects/deployments/_actions.haml
+++ b/app/views/projects/deployments/_actions.haml
@@ -1,12 +1,18 @@
- if can?(current_user, :create_deployment, deployment) && deployment.deployable
.pull-right
+
+ - external_url = deployment.environment.external_url
+ - if external_url
+ = link_to external_url, target: '_blank', class: 'btn external-url' do
+ = icon('external-link')
+
- actions = deployment.manual_actions
- if actions.present?
.inline
.dropdown
%a.dropdown-new.btn.btn-default{type: 'button', 'data-toggle' => 'dropdown'}
= custom_icon('icon_play')
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- actions.each do |action|
%li
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index cd95841ca5a..ca0005abd0c 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -5,14 +5,16 @@
%td
= render 'projects/deployments/commit', deployment: deployment
- %td
+ %td.build-column
- if deployment.deployable
- = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable] do
- = user_avatar(user: deployment.user, size: 20)
+ = link_to [@project.namespace.becomes(Namespace), @project, deployment.deployable], class: 'build-link' do
= "#{deployment.deployable.name} (##{deployment.deployable.id})"
+ - if deployment.user
+ by
+ = user_avatar(user: deployment.user, size: 20)
%td
#{time_ago_with_tooltip(deployment.created_at)}
- %td
+ %td.hidden-xs
= render 'projects/deployments/actions', deployment: deployment, allow_rollback: true
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index d37961c4e40..779c8ea0104 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -11,7 +11,9 @@
- elsif diff_file.collapsed?
- url = url_for(params.merge(action: :diff_for_path, old_path: diff_file.old_path, new_path: diff_file.new_path))
.nothing-here-block.diff-collapsed{data: { diff_for_path: url } }
- This diff is collapsed. Click to expand it.
+ This diff is collapsed.
+ %a.click-to-expand
+ Click to expand it.
- elsif diff_file.diff_lines.length > 0
- if diff_view == :parallel
= render "projects/diffs/parallel_view", diff_file: diff_file, project: project, blob: blob
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 62aff36aadd..067cf595da3 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -1,7 +1,6 @@
- show_whitespace_toggle = local_assigns.fetch(:show_whitespace_toggle, true)
+- can_create_note = !@diff_notes_disabled && can?(current_user, :create_note, diffs.project)
- diff_files = diffs.diff_files
-- if diff_view == :parallel
- - fluid_layout true
.content-block.oneline-block.files-changed
.inline-parallel-buttons
@@ -22,7 +21,7 @@
- if diff_files.overflow?
= render 'projects/diffs/warning', diff_files: diff_files
-.files{data: {can_create_note: (!@diff_notes_disabled && can?(current_user, :create_note, diffs.project))}}
+.files{ data: { can_create_note: can_create_note } }
- diff_files.each_with_index do |diff_file, index|
- diff_commit = commit_for_diff(diff_file)
- blob = diff_file.blob(diff_commit)
diff --git a/app/views/projects/diffs/_file.html.haml b/app/views/projects/diffs/_file.html.haml
index 1a51ccd4c7d..257e0a855bd 100644
--- a/app/views/projects/diffs/_file.html.haml
+++ b/app/views/projects/diffs/_file.html.haml
@@ -5,10 +5,10 @@
- unless diff_file.submodule?
.file-actions.hidden-xs
- if blob_text_viewable?(blob)
- = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this files", disabled: @diff_notes_disabled do
+ = link_to '#', class: 'js-toggle-diff-comments btn active has-tooltip btn-file-option', title: "Toggle comments for this file", disabled: @diff_notes_disabled do
= icon('comment')
\
-
+ = clipboard_button(clipboard_text: diff_file.new_path, class: 'btn-file-option')
- if editable_diff?(diff_file)
- link_opts = @merge_request.id ? { from_merge_request_id: @merge_request.id } : {}
= edit_blob_link(@merge_request.source_project, @merge_request.source_branch, diff_file.new_path,
diff --git a/app/views/projects/diffs/_file_header.html.haml b/app/views/projects/diffs/_file_header.html.haml
index 95a2772fd0b..a6a2e5690b5 100644
--- a/app/views/projects/diffs/_file_header.html.haml
+++ b/app/views/projects/diffs/_file_header.html.haml
@@ -1,3 +1,4 @@
+%i.fa.diff-toggle-caret
- if defined?(blob) && blob && diff_file.submodule?
%span
= icon('archive fw')
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index a04d53e02bf..d19422c8657 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -100,7 +100,8 @@
= f.check_box :container_registry_enabled
%strong Container Registry
%br
- %span.descr Enable Container Registry for this repository
+ %span.descr Enable Container Registry for this project
+ = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
= render 'merge_request_settings', f: f
%hr
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 636beb73ec2..7a39064adc5 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -23,6 +23,8 @@
or a
= link_to '.gitignore', add_special_file_path(@project, file_name: '.gitignore'), class: 'underlined-link'
to this project.
+ %p
+ You will need to be owner or have the master permission level for the initial push, as the master branch is automatically protected.
- if can?(current_user, :push_code, @project)
%div{ class: container_class }
diff --git a/app/views/projects/environments/_environment.html.haml b/app/views/projects/environments/_environment.html.haml
index 36a6162a5a8..251694e897c 100644
--- a/app/views/projects/environments/_environment.html.haml
+++ b/app/views/projects/environments/_environment.html.haml
@@ -4,10 +4,17 @@
%td
= link_to environment.name, namespace_project_environment_path(@project.namespace, @project, environment)
- %td
+ %td.deployment-column
- if last_deployment
- = user_avatar(user: last_deployment.user, size: 20)
- %strong ##{last_deployment.id}
+ %span ##{last_deployment.iid}
+ - if last_deployment.user
+ by
+ = user_avatar(user: last_deployment.user, size: 20)
+
+ %td
+ - if last_deployment && last_deployment.deployable
+ = link_to [@project.namespace.becomes(Namespace), @project, last_deployment.deployable], class: 'build-link' do
+ = "#{last_deployment.deployable.name} (##{last_deployment.deployable.id})"
%td
- if last_deployment
@@ -20,5 +27,5 @@
- if last_deployment
#{time_ago_with_tooltip(last_deployment.created_at)}
- %td
+ %td.hidden-xs
= render 'projects/deployments/actions', deployment: last_deployment
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index b3eb5b0011a..ab801409722 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -9,25 +9,27 @@
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
New environment
- - if @environments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any environments right now.
- %p.blank-state-text
- Environments are places where code gets deployed, such as staging or production.
- %br
- = succeed "." do
- = link_to "Read more about environments", help_page_path("ci/environments")
- - if can?(current_user, :create_environment, @project)
- = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
- New environment
- - else
- .table-holder
- %table.table.builds.environments
- %tbody
- %th Environment
- %th Last Deployment
- %th Commit
- %th
- %th
- = render @environments
+ .environments-container
+ - if @environments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any environments right now.
+ %p.blank-state-text
+ Environments are places where code gets deployed, such as staging or production.
+ %br
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci/environments")
+ - if can?(current_user, :create_environment, @project)
+ = link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
+ New environment
+ - else
+ .table-holder
+ %table.table.builds.environments
+ %tbody
+ %th Environment
+ %th Last Deployment
+ %th Build
+ %th Commit
+ %th
+ %th.hidden-xs
+ = render @environments
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 8f8c1c4ce22..7a8d196cf4e 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -12,26 +12,27 @@
= link_to 'Edit', edit_namespace_project_environment_path(@project.namespace, @project, @environment), class: 'btn'
= link_to 'Destroy', namespace_project_environment_path(@project.namespace, @project, @environment), data: { confirm: 'Are you sure you want to delete this environment?' }, class: 'btn btn-danger', method: :delete
- - if @deployments.blank?
- .blank-state.blank-state-no-icon
- %h2.blank-state-title
- You don't have any deployments right now.
- %p.blank-state-text
- Define environments in the deploy stage(s) in
- %code .gitlab-ci.yml
- to track deployments here.
- = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
- - else
- .table-holder
- %table.table.builds.environments
- %thead
- %tr
- %th ID
- %th Commit
- %th Build
- %th
- %th
+ .deployments-container
+ - if @deployments.blank?
+ .blank-state.blank-state-no-icon
+ %h2.blank-state-title
+ You don't have any deployments right now.
+ %p.blank-state-text
+ Define environments in the deploy stage(s) in
+ %code .gitlab-ci.yml
+ to track deployments here.
+ = link_to "Read more", help_page_path("ci/environments"), class: "btn btn-success"
+ - else
+ .table-holder
+ %table.table.builds.environments
+ %thead
+ %tr
+ %th ID
+ %th Commit
+ %th Build
+ %th
+ %th.hidden-xs
- = render @deployments
+ = render @deployments
- = paginate @deployments, theme: 'gitlab'
+ = paginate @deployments, theme: 'gitlab'
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index bacc5708e4b..abf4f697f86 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -15,7 +15,7 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
- excluded_filters = [:state, :scope, :label_name, :milestone_id, :assignee_id, :author_id]
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 331dc1fcc29..80fe6be49b0 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
@@ -62,5 +62,3 @@
%td.coverage
- if generic_commit_status.try(:coverage)
#{generic_commit_status.coverage}%
-
- %td
diff --git a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
index 409f4701e4b..0a66d60accc 100644
--- a/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
+++ b/app/views/projects/generic_commit_statuses/_generic_commit_status_pipeline.html.haml
@@ -1,7 +1,9 @@
- if subject.target_url
= link_to subject.target_url do
- = render_status_with_link('commit status', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('commit status', subject.status)
%span.ci-status-text= subject.name
- else
- = render_status_with_link('commit status', subject.status)
+ %span.ci-status-icon
+ = render_status_with_link('commit status', subject.status)
%span.ci-status-text= subject.name
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 082e2cb4d8c..1a62a6a809c 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,18 +1,19 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
- - content_for :page_specific_javascripts do
- = page_specific_javascript_tag('lib/chart.js')
- = page_specific_javascript_tag('graphs/graphs_bundle.js')
- = nav_link(action: :show) do
- = link_to 'Contributors', namespace_project_graph_path
- = nav_link(action: :commits) do
- = link_to 'Commits', commits_namespace_project_graph_path
- = nav_link(action: :languages) do
- = link_to 'Languages', languages_namespace_project_graph_path
- - if @project.feature_available?(:builds, current_user)
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+ - content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+ = nav_link(action: :show) do
+ = link_to 'Contributors', namespace_project_graph_path
+ = nav_link(action: :commits) do
+ = link_to 'Commits', commits_namespace_project_graph_path
+ = nav_link(action: :languages) do
+ = link_to 'Languages', languages_namespace_project_graph_path
+ - if @project.feature_available?(:builds, current_user)
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/group_links/index.html.haml b/app/views/projects/group_links/index.html.haml
index ca700cb3a3b..1b0dbbb8111 100644
--- a/app/views/projects/group_links/index.html.haml
+++ b/app/views/projects/group_links/index.html.haml
@@ -8,15 +8,15 @@
.col-lg-9
%h5.prepend-top-0
Set a group to share
- = form_tag namespace_project_group_links_path(@project.namespace, @project), method: :post do
+ = form_tag namespace_project_group_links_path(@project.namespace, @project), class: 'js-requires-input', method: :post do
.form-group
= label_tag :link_group_id, "Group", class: "label-light"
- = groups_select_tag(:link_group_id, skip_group: @project.group.try(:path))
+ = groups_select_tag(:link_group_id, data: { skip_groups: @skip_groups }, required: true)
.form-group
= label_tag :link_group_access, "Max access level", class: "label-light"
.select-wrapper
= select_tag :link_group_access, options_for_select(ProjectGroupLink.access_options, ProjectGroupLink.default_access), class: "form-control select-control"
- %span.caret
+ = icon('caret-down')
.form-group
= label_tag :expires_at, 'Access expiration date', class: 'label-light'
.clearable-input
diff --git a/app/views/projects/issues/_head.html.haml b/app/views/projects/issues/_head.html.haml
index f88b33018d0..509b01c548a 100644
--- a/app/views/projects/issues/_head.html.haml
+++ b/app/views/projects/issues/_head.html.haml
@@ -1,32 +1,33 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
- = nav_link(controller: :issues) do
- = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
- %span
- Issues
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab?(:issues) && !current_controller?(:merge_requests)
+ = nav_link(controller: :issues) do
+ = link_to namespace_project_issues_path(@project.namespace, @project), title: 'Issues' do
+ %span
+ Issues
- = nav_link(controller: :boards) do
- = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
- %span
- Board
+ = nav_link(controller: :boards) do
+ = link_to namespace_project_board_path(@project.namespace, @project), title: 'Board' do
+ %span
+ Board
- - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
- = nav_link(controller: :merge_requests) do
- = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
- %span
- Merge Requests
+ - if project_nav_tab?(:merge_requests) && current_controller?(:merge_requests)
+ = nav_link(controller: :merge_requests) do
+ = link_to namespace_project_merge_requests_path(@project.namespace, @project), title: 'Merge Requests' do
+ %span
+ Merge Requests
- - if project_nav_tab? :labels
- = nav_link(controller: :labels) do
- = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
- %span
- Labels
+ - if project_nav_tab? :labels
+ = nav_link(controller: :labels) do
+ = link_to namespace_project_labels_path(@project.namespace, @project), title: 'Labels' do
+ %span
+ Labels
- - if project_nav_tab? :milestones
- = nav_link(controller: :milestones) do
- = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
- %span
- Milestones \ No newline at end of file
+ - if project_nav_tab? :milestones
+ = nav_link(controller: :milestones) do
+ = link_to namespace_project_milestones_path(@project.namespace, @project), title: 'Milestones' do
+ %span
+ Milestones
diff --git a/app/views/projects/issues/edit.html.haml b/app/views/projects/issues/edit.html.haml
index 7cf1923456e..3a6fbbc7fbc 100644
--- a/app/views/projects/issues/edit.html.haml
+++ b/app/views/projects/issues/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "Edit", "#{@issue.to_reference} #{@issue.title}", "Issues"
%h3.page-title
Edit Issue ##{@issue.iid}
diff --git a/app/views/projects/issues/index.html.haml b/app/views/projects/issues/index.html.haml
index 8da9f2100e9..cc57cfdb342 100644
--- a/app/views/projects/issues/index.html.haml
+++ b/app/views/projects/issues/index.html.haml
@@ -3,7 +3,8 @@
- page_title "Issues"
- new_issue_email = @project.new_issue_address(current_user)
-= render "projects/issues/head"
+= content_for :sub_nav do
+ = render "projects/issues/head"
= content_for :meta_tags do
- if current_user
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 3fb4191c60e..09347ad5fff 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -1,4 +1,4 @@
-- page_title "#{@issue.title} (##{@issue.iid})", "Issues"
+- page_title "#{@issue.to_reference} #{@issue.title}", "Issues"
- page_description @issue.description
- page_card_attributes @issue.card_attributes
@@ -23,8 +23,8 @@
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- %span.caret
Options
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
- if can?(current_user, :create_issue, @project)
@@ -55,12 +55,12 @@
.issue-details.issuable-details
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@issue.title), pipeline: :single_line, author: @issue.author
+ = markdown_field(@issue, :title)
- if @issue.description.present?
.description{ class: can?(current_user, :update_issue, @issue) ? 'js-task-list-container' : '' }
.wiki
= preserve do
- = markdown(@issue.description, cache_key: [@issue, "description"], author: @issue.author)
+ = markdown_field(@issue, :description)
%textarea.hidden.js-task-list-field
= @issue.description
= edited_time_ago_with_tooltip(@issue, placement: 'bottom', html_class: 'issue_edited_ago')
diff --git a/app/views/projects/labels/_form.html.haml b/app/views/projects/labels/_form.html.haml
index aa143e54ffe..6ab6ae50389 100644
--- a/app/views/projects/labels/_form.html.haml
+++ b/app/views/projects/labels/_form.html.haml
@@ -14,7 +14,7 @@
.col-sm-10
.input-group
.input-group-addon.label-color-preview &nbsp;
- = f.color_field :color, class: "form-control"
+ = f.text_field :color, class: "form-control"
.help-block
Choose any color.
%br
diff --git a/app/views/projects/labels/_label.html.haml b/app/views/projects/labels/_label.html.haml
index 73c6f2a046c..71f7f354d72 100644
--- a/app/views/projects/labels/_label.html.haml
+++ b/app/views/projects/labels/_label.html.haml
@@ -5,7 +5,7 @@
.visible-xs.visible-sm-inline-block.visible-md-inline-block.dropdown
%button.btn.btn-default.label-options-toggle{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right
%ul
%li
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 3900b4f6f17..cfb44bd206c 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -5,7 +5,7 @@
- if @merge_request.reopenable?
= link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-reopen reopen-mr-link js-note-target-reopen", title: "Reopen merge request", data: {original_text: "Reopen merge request", alternative_text: "Comment & reopen merge request"}
%comment-and-resolve-btn{ "inline-template" => true, ":discussion-id" => "" }
- %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { namespace_path: "#{@merge_request.project.namespace.path}", project_path: "#{@merge_request.project.path}" } }
+ %button.btn.btn-nr.btn-default.append-right-10.js-comment-resolve-button{ "v-if" => "showButton", type: "submit", data: { project_path: "#{project_path(@merge_request.project)}" } }
{{ buttonText }}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index de39964fca8..466ec1475d8 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -65,19 +65,6 @@
- if @merge_request.errors.any?
= form_errors(@merge_request)
- - elsif @merge_request.source_branch.present? && @merge_request.target_branch.present?
- .light-well.append-bottom-default
- .center
- %h4
- There isn't anything to merge.
- %p.slead
- - if @merge_request.source_branch == @merge_request.target_branch
- You'll need to use different branch names to get a valid comparison.
- - else
- %span.label-branch #{@merge_request.source_branch}
- and
- %span.label-branch #{@merge_request.target_branch}
- are the same.
= f.submit 'Compare branches and continue', class: "btn btn-new mr-compare-btn"
:javascript
diff --git a/app/views/projects/merge_requests/_new_diffs.html.haml b/app/views/projects/merge_requests/_new_diffs.html.haml
new file mode 100644
index 00000000000..74367ab9b7b
--- /dev/null
+++ b/app/views/projects/merge_requests/_new_diffs.html.haml
@@ -0,0 +1 @@
+= render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
diff --git a/app/views/projects/merge_requests/_new_submit.html.haml b/app/views/projects/merge_requests/_new_submit.html.haml
index 00bd4e143df..da6927879a4 100644
--- a/app/views/projects/merge_requests/_new_submit.html.haml
+++ b/app/views/projects/merge_requests/_new_submit.html.haml
@@ -18,34 +18,38 @@
= f.hidden_field :target_branch
.mr-compare.merge-request
- %ul.merge-request-tabs.nav-links.no-top.no-bottom
- %li.commits-tab
- = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
- Commits
- %span.badge= @commits.size
- - if @pipeline
- %li.builds-tab.active
- = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
- Builds
- %span.badge= @statuses.size
- %li.diffs-tab.active
- = link_to url_for(params), data: {target: 'div#diffs', action: 'diffs', toggle: 'tab'} do
- Changes
- %span.badge= @diffs.real_size
+ - if @commits.empty?
+ .commits-empty
+ %h4
+ There are no commits yet.
+ = custom_icon ('illustration_no_commits')
+ - else
+ %ul.merge-request-tabs.nav-links.no-top.no-bottom
+ %li.commits-tab.active
+ = link_to url_for(params), data: {target: 'div#commits', action: 'new', toggle: 'tab'} do
+ Commits
+ %span.badge= @commits.size
+ - if @pipeline
+ %li.builds-tab
+ = link_to url_for(params), data: {target: 'div#builds', action: 'builds', toggle: 'tab'} do
+ Builds
+ %span.badge= @statuses.size
+ %li.diffs-tab
+ = link_to url_for(params.merge(action: 'new_diffs')), data: {target: 'div#diffs', action: 'new/diffs', toggle: 'tab'} do
+ Changes
+ %span.badge= @merge_request.diff_size
- .tab-content
- #commits.commits.tab-pane
- = render "projects/merge_requests/show/commits"
- #diffs.diffs.tab-pane.active
- - if @commits.size > MergeRequestDiff::COMMITS_SAFE_SIZE
- .alert.alert-danger
- %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
- %p To preserve performance the line changes are not shown.
- - else
- = render "projects/diffs/diffs", diffs: @diffs, show_whitespace_toggle: false
- - if @pipeline
- #builds.builds.tab-pane
- = render "projects/merge_requests/show/builds"
+ .tab-content
+ #commits.commits.tab-pane.active
+ = render "projects/merge_requests/show/commits"
+ #diffs.diffs.tab-pane
+ - # This tab is always loaded via AJAX
+ - if @pipeline
+ #builds.builds.tab-pane
+ = render "projects/merge_requests/show/builds"
+
+ .mr-loading-status
+ = spinner
:javascript
$('.assign-to-me-link').on('click', function(e){
@@ -54,6 +58,6 @@
});
:javascript
var merge_request = new MergeRequest({
- action: "#{(@show_changes_tab ? 'diffs' : 'new')}",
- setUrl: false
+ action: "#{(@show_changes_tab ? 'new/diffs' : 'new')}",
+ buildsLoaded: "#{@pipeline ? 'true' : 'false'}"
});
diff --git a/app/views/projects/merge_requests/_show.html.haml b/app/views/projects/merge_requests/_show.html.haml
index d03ff9ec7e8..47dd51639b5 100644
--- a/app/views/projects/merge_requests/_show.html.haml
+++ b/app/views/projects/merge_requests/_show.html.haml
@@ -1,12 +1,9 @@
-- page_title "#{@merge_request.title} (#{@merge_request.to_reference})", "Merge Requests"
+- page_title "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests"
- page_description @merge_request.description
- page_card_attributes @merge_request.card_attributes
- content_for :page_specific_javascripts do
= page_specific_javascript_tag('diff_notes/diff_notes_bundle.js')
-- if diff_view == :parallel
- - fluid_layout true
-
.merge-request{'data-url' => merge_request_path(@merge_request)}
= render "projects/merge_requests/show/mr_title"
@@ -25,7 +22,7 @@
%span.dropdown.inline.prepend-left-5
%a.btn.btn-sm.dropdown-toggle{ data: {toggle: :dropdown} }
Download as
- %span.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li= link_to "Email Patches", merge_request_path(@merge_request, format: :patch)
%li= link_to "Plain Diff", merge_request_path(@merge_request, format: :diff)
diff --git a/app/views/projects/merge_requests/edit.html.haml b/app/views/projects/merge_requests/edit.html.haml
index 03159f123f3..7c3ac6652ee 100644
--- a/app/views/projects/merge_requests/edit.html.haml
+++ b/app/views/projects/merge_requests/edit.html.haml
@@ -1,4 +1,4 @@
-- page_title "Edit", "#{@merge_request.title} (#{@merge_request.to_reference}", "Merge Requests"
+- page_title "Edit", "#{@merge_request.to_reference} #{@merge_request.title}", "Merge Requests"
%h3.page-title
Edit Merge Request #{@merge_request.to_reference}
diff --git a/app/views/projects/merge_requests/show/_mr_box.html.haml b/app/views/projects/merge_requests/show/_mr_box.html.haml
index ebf18f6ac85..ed23d06ee5e 100644
--- a/app/views/projects/merge_requests/show/_mr_box.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_box.html.haml
@@ -1,13 +1,13 @@
.detail-page-description.content-block
%h2.title
- = markdown escape_once(@merge_request.title), pipeline: :single_line, author: @merge_request.author
+ = markdown_field(@merge_request, :title)
%div
- if @merge_request.description.present?
.description{class: can?(current_user, :update_merge_request, @merge_request) ? 'js-task-list-container' : ''}
.wiki
= preserve do
- = markdown(@merge_request.description, cache_key: [@merge_request, "description"], author: @merge_request.author)
+ = markdown_field(@merge_request, :description)
%textarea.hidden.js-task-list-field
= @merge_request.description
diff --git a/app/views/projects/merge_requests/show/_mr_title.html.haml b/app/views/projects/merge_requests/show/_mr_title.html.haml
index e35291dff7d..e7c5bca6a37 100644
--- a/app/views/projects/merge_requests/show/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/show/_mr_title.html.haml
@@ -19,8 +19,8 @@
.issuable-actions
.clearfix.issue-btn-group.dropdown
%button.btn.btn-default.pull-left.hidden-md.hidden-lg{ type: "button", data: { toggle: "dropdown" } }
- %span.caret
Options
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-align-right.hidden-lg
%ul
%li{ class: merge_request_button_visibility(@merge_request, true) }
diff --git a/app/views/projects/merge_requests/show/_versions.html.haml b/app/views/projects/merge_requests/show/_versions.html.haml
index 49819519759..988ac0feae1 100644
--- a/app/views/projects/merge_requests/show/_versions.html.haml
+++ b/app/views/projects/merge_requests/show/_versions.html.haml
@@ -9,7 +9,7 @@
latest version
- else
version #{version_index(@merge_request_diff)}
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Version:
@@ -39,7 +39,7 @@
version #{version_index(@start_version)}
- else
#{@merge_request.target_branch}
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-select.dropdown-menu-selectable
.dropdown-title
%span Compared with:
@@ -58,11 +58,11 @@
.monospace #{short_sha(merge_request_diff.head_commit_sha)}
%small
= time_ago_with_tooltip(merge_request_diff.created_at)
- %li
- = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
- %strong
- #{@merge_request.target_branch} (base)
- .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
+ %li
+ = link_to merge_request_version_path(@project, @merge_request, @merge_request_diff), class: ('is-active' unless @start_sha) do
+ %strong
+ #{@merge_request.target_branch} (base)
+ .monospace #{short_sha(@merge_request_diff.base_commit_sha)}
- unless @merge_request_diff.latest? && !@start_sha
.comments-disabled-notif.content-block
diff --git a/app/views/projects/merge_requests/widget/_heading.html.haml b/app/views/projects/merge_requests/widget/_heading.html.haml
index 44e645a7e81..5b7f83c344f 100644
--- a/app/views/projects/merge_requests/widget/_heading.html.haml
+++ b/app/views/projects/merge_requests/widget/_heading.html.haml
@@ -4,14 +4,15 @@
.ci_widget{ class: "ci-#{status}", style: ("display:none" unless @pipeline.status == status) }
= ci_icon_for_status(status)
%span
- CI build
+ Pipeline
+ = link_to "##{@pipeline.id}", namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'pipeline'
= ci_label_for_status(status)
for
- commit = @merge_request.diff_head_commit
= succeed "." do
= link_to @pipeline.short_sha, namespace_project_commit_path(@merge_request.source_project.namespace, @merge_request.source_project, @pipeline.sha), class: "monospace"
%span.ci-coverage
- = link_to "View details", builds_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'builds'}
+ = link_to "View details", pipelines_namespace_project_merge_request_path(@project.namespace, @project, @merge_request), class: "js-show-tab", data: {action: 'pipelines'}
- elsif @merge_request.has_ci?
- # Compatibility with old CI integrations (ex jenkins) when you request status from CI server via AJAX
@@ -48,11 +49,12 @@
.mr-widget-heading
.ci_widget.ci-success
= ci_icon_for_status("success")
- %span.hidden-sm
+ %span
Deployed to
= succeed '.' do
= link_to environment.name, environment_path(environment), class: 'environment'
- external_url = environment.external_url
- if external_url
= link_to external_url, target: '_blank' do
- = icon('external-link', text: "View on #{external_url.gsub(/\A.*?:\/\//, '')}", right: true)
+ %span.hidden-xs View on #{external_url.gsub(/\A.*?:\/\//, '')}
+ = icon('external-link', right: true)
diff --git a/app/views/projects/merge_requests/widget/open/_accept.html.haml b/app/views/projects/merge_requests/widget/open/_accept.html.haml
index bf2e76f0083..ce43ca3a286 100644
--- a/app/views/projects/merge_requests/widget/open/_accept.html.haml
+++ b/app/views/projects/merge_requests/widget/open/_accept.html.haml
@@ -12,7 +12,7 @@
Merge When Build Succeeds
- unless @project.only_allow_merge_if_build_succeeds?
= button_tag class: "btn btn-success dropdown-toggle", 'data-toggle' => 'dropdown' do
- %span.caret
+ = icon('caret-down')
%span.sr-only
Select Merge Moment
%ul.js-merge-dropdown.dropdown-menu.dropdown-menu-right{ role: 'menu' }
diff --git a/app/views/projects/milestones/show.html.haml b/app/views/projects/milestones/show.html.haml
index 73772cc0e32..e62f810a521 100644
--- a/app/views/projects/milestones/show.html.haml
+++ b/app/views/projects/milestones/show.html.haml
@@ -30,13 +30,13 @@
.detail-page-description.milestone-detail
%h2.title
- = markdown escape_once(@milestone.title), pipeline: :single_line
+ = markdown_field(@milestone, :title)
%div
- if @milestone.description.present?
.description
.wiki
= preserve do
- = markdown @milestone.description
+ = markdown_field(@milestone, :description)
- if @milestone.total_items_count(current_user).zero?
.alert.alert-success.prepend-top-default
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index b2ece44d966..29df1bab04e 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -8,7 +8,7 @@
.project-network
.controls
= form_tag namespace_project_network_path(@project.namespace, @project, @id), method: :get, class: 'form-inline network-form' do |f|
- = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Input an extended SHA1 syntax", class: 'search-input form-control input-mx-250 search-sha'
+ = text_field_tag :extended_sha1, @options[:extended_sha1], placeholder: "Git revision", class: 'search-input form-control input-mx-250 search-sha'
= button_tag class: 'btn btn-success' do
= icon('search')
.inline.prepend-left-20
diff --git a/app/views/projects/new.html.haml b/app/views/projects/new.html.haml
index fda0592dd41..cc8cb134fb8 100644
--- a/app/views/projects/new.html.haml
+++ b/app/views/projects/new.html.haml
@@ -72,7 +72,7 @@
= link_to "#", class: 'btn js-toggle-button import_git' do
= icon('git', text: 'Repo by URL')
%div{ class: 'import_gitlab_project' }
- - if gitlab_project_import_enabled? && current_user.is_admin?
+ - if gitlab_project_import_enabled?
= link_to new_import_gitlab_project_path, class: 'btn btn_import_gitlab_project project-submit' do
= icon('gitlab', text: 'GitLab export')
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index 9ec17cf6e76..73fe6a715fa 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -24,14 +24,12 @@
- if note.resolvable?
- can_resolve = can?(current_user, :resolve_note, note)
-
- %resolve-btn{ ":namespace-path" => "'#{note.project.namespace.path}'",
- ":project-path" => "'#{note.project.path}'",
- ":discussion-id" => "'#{note.discussion_id}'",
+ %resolve-btn{ "project-path" => "#{project_path(note.project)}",
+ "discussion-id" => "#{note.discussion_id}",
":note-id" => note.id,
":resolved" => note.resolved?,
":can-resolve" => can_resolve,
- ":resolved-by" => "'#{note.resolved_by.try(:name)}'",
+ "resolved-by" => "#{note.resolved_by.try(:name)}",
"v-show" => "#{can_resolve || note.resolved?}",
"inline-template" => true,
"v-ref:note_#{note.id}" => true }
@@ -63,7 +61,7 @@
.note-body{class: note_editable ? 'js-task-list-container' : ''}
.note-text.md
= preserve do
- = note.note_html
+ = note.redacted_note_html
= edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- if note_editable
= render 'projects/notes/edit_form', note: note
diff --git a/app/views/projects/pipelines/_head.html.haml b/app/views/projects/pipelines/_head.html.haml
index 5f571499e80..7d421c0e740 100644
--- a/app/views/projects/pipelines/_head.html.haml
+++ b/app/views/projects/pipelines/_head.html.haml
@@ -1,27 +1,28 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- - if project_nav_tab? :pipelines
- = nav_link(controller: :pipelines) do
- = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
- %span
- Pipelines
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ - if project_nav_tab? :pipelines
+ = nav_link(controller: :pipelines) do
+ = link_to project_pipelines_path(@project), title: 'Pipelines', class: 'shortcuts-pipelines' do
+ %span
+ Pipelines
- - if project_nav_tab? :builds
- = nav_link(controller: %w(builds)) do
- = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
- %span
- Builds
+ - if project_nav_tab? :builds
+ = nav_link(controller: %w(builds)) do
+ = link_to project_builds_path(@project), title: 'Builds', class: 'shortcuts-builds' do
+ %span
+ Builds
- - if project_nav_tab? :environments
- = nav_link(controller: %w(environments)) do
- = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
- %span
- Environments
+ - if project_nav_tab? :environments
+ = nav_link(controller: %w(environments)) do
+ = link_to project_environments_path(@project), title: 'Environments', class: 'shortcuts-environments' do
+ %span
+ Environments
- - if can?(current_user, :read_cycle_analytics, @project)
- = nav_link(controller: %w(cycle_analytics)) do
- = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
- %span
- Cycle Analytics
+ - if can?(current_user, :read_cycle_analytics, @project)
+ = nav_link(controller: %w(cycle_analytics)) do
+ = link_to project_cycle_analytics_path(@project), title: 'Cycle Analytics' do
+ %span
+ Cycle Analytics
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 5800ef7de48..d288efc546f 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -33,7 +33,7 @@
- if @commit
.commit-box.content-block
%h3.commit-title
- = markdown escape_once(@commit.title), pipeline: :single_line
+ = markdown(@commit.title, pipeline: :single_line)
- if @commit.description.present?
%pre.commit-description
- = preserve(markdown(escape_once(@commit.description), pipeline: :single_line))
+ = preserve(markdown(@commit.description, pipeline: :single_line))
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index faf28db68d1..2d1df095bfa 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -36,20 +36,20 @@
= link_to ci_lint_path, class: 'btn btn-default' do
%span CI Lint
- %ul.content-list.pipelines
+ %div.content-list.pipelines
- stages = @pipelines.stages
- if @pipelines.blank?
- %li
+ %div
.nothing-here-block No pipelines to show
- else
.table-holder
%table.table.builds
- %tbody
- %th Status
- %th Commit
- %th Stages
- %th
- %th
+ %thead
+ %th.col-xs-1.col-sm-1 Status
+ %th.col-xs-2.col-sm-4 Pipeline
+ %th.col-xs-2.col-sm-2 Stages
+ %th.col-xs-2.col-sm-2
+ %th.hidden-xs.col-sm-3
= render @pipelines, commit_sha: true, stage: true, allow_retry: true, stages: stages
= paginate @pipelines, theme: 'gitlab'
diff --git a/app/views/projects/repositories/_feed.html.haml b/app/views/projects/repositories/_feed.html.haml
index 43a6fdfd103..d9c39fb87b7 100644
--- a/app/views/projects/repositories/_feed.html.haml
+++ b/app/views/projects/repositories/_feed.html.haml
@@ -12,7 +12,7 @@
= link_to namespace_project_commits_path(@project.namespace, @project, commit.id) do
%code= commit.short_id
= image_tag avatar_icon(commit.author_email), class: "", width: 16, alt: ''
- = markdown escape_once(truncate(commit.title, length: 40)), pipeline: :single_line, author: commit.author
+ = markdown(truncate(commit.title, length: 40), pipeline: :single_line, author: commit.author)
%td
%span.pull-right.cgray
= time_ago_with_tooltip(commit.committed_date)
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index c45a9d4f81f..33a9a96183c 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -5,7 +5,7 @@
.col-sm-10
.checkbox
= f.check_box :active
- %span.light Paused runners don't accept new builds
+ %span.light Paused Runners don't accept new builds
.form-group
= label :run_untagged, 'Run untagged jobs', class: 'control-label'
.col-sm-10
@@ -33,6 +33,6 @@
Tags
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.to_s, class: 'form-control'
- .help-block You can setup jobs to only use runners with specific tags
+ .help-block You can setup jobs to only use Runners with specific tags
.form-actions
= f.submit 'Save changes', class: 'btn btn-save'
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 85225857758..6e58e5a0c78 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -15,7 +15,7 @@
.pull-right
- if @project_runners.include?(runner)
- if runner.belongs_to_one_project?
- = link_to 'Remove runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
+ = link_to 'Remove Runner', runner_path(runner), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
- else
- runner_project = @project.runner_projects.find_by(runner_id: runner)
= link_to 'Disable for this project', namespace_project_runner_project_path(@project.namespace, @project, runner_project), data: { confirm: "Are you sure?" }, method: :delete, class: 'btn btn-danger btn-sm'
diff --git a/app/views/projects/runners/_shared_runners.html.haml b/app/views/projects/runners/_shared_runners.html.haml
index 9fa4127c948..5afa193357e 100644
--- a/app/views/projects/runners/_shared_runners.html.haml
+++ b/app/views/projects/runners/_shared_runners.html.haml
@@ -1,24 +1,26 @@
-%h3 Shared runners
+%h3 Shared Runners
.bs-callout.bs-callout-warning.shared-runners-description
- - if shared_runners_text.present?
- = markdown(shared_runners_text, pipeline: 'plain_markdown')
+ - if current_application_settings.shared_runners_text.present?
+ = markdown_field(current_application_settings, :shared_runners_text)
- else
- Shared runners execute code of different projects on the same Runner unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is on GitLab.com).
+ GitLab Shared Runners execute code of different projects on the same Runner
+ unless you configure GitLab Runner Autoscale with MaxBuilds 1 (which it is
+ on GitLab.com).
%hr
- if @project.shared_runners_enabled?
= link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-warning', method: :post do
- Disable shared runners
+ Disable shared Runners
- else
= link_to toggle_shared_runners_namespace_project_runners_path(@project.namespace, @project), class: 'btn btn-success', method: :post do
- Enable shared runners
+ Enable shared Runners
&nbsp; for this project
- if @shared_runners_count.zero?
- This GitLab server does not provide any shared runners yet.
- Please use specific runners or ask the administrator to create one.
+ This GitLab server does not provide any shared Runners yet.
+ Please use the specific Runners or ask your administrator to create one.
- else
- %h4.underlined-title Available shared runners - #{@shared_runners_count}
+ %h4.underlined-title Available shared Runners : #{@shared_runners_count}
%ul.bordered-list.available-shared-runners
= render partial: 'runner', collection: @shared_runners, as: :runner
- if @shared_runners_count > 10
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index d469dda5b81..858af78f7bf 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -1,20 +1,20 @@
-%h3 Specific runners
+%h3 Specific Runners
.bs-callout.help-callout
- %h4 How to setup a new project specific runner
+ %h4 How to setup a specific Runner for a new project
%ol
%li
- Install GitLab Runner software.
- Checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} to install it
+ Install a Runner compatible with GitLab CI
+ (checkout the #{link_to 'GitLab Runner section', 'https://about.gitlab.com/gitlab-ci/#gitlab-runner', target: '_blank'} for information on how to install it).
%li
- Specify the following URL during runner setup:
+ Specify the following URL during the Runner setup:
%code #{ci_root_url(only_path: false)}
%li
Use the following registration token during setup:
%code #{@project.runners_token}
%li
- Start runner!
+ Start the Runner!
- if @project_runners.any?
diff --git a/app/views/projects/runners/index.html.haml b/app/views/projects/runners/index.html.haml
index 2d5b9f43c24..92957470070 100644
--- a/app/views/projects/runners/index.html.haml
+++ b/app/views/projects/runners/index.html.haml
@@ -2,24 +2,24 @@
.light.prepend-top-default
%p
- A 'runner' is a process which runs a build.
- You can setup as many runners as you need.
+ A 'Runner' is a process which runs a build.
+ You can setup as many Runners as you need.
%br
Runners can be placed on separate users, servers, and even on your local machine.
- %p Each runner can be in one of the following states:
+ %p Each Runner can be in one of the following states:
%div
%ul
%li
%span.label.label-success active
- \- runner is active and can process any new build
+ \- Runner is active and can process any new builds
%li
%span.label.label-danger paused
- \- runner is paused and will not receive any new build
+ \- Runner is paused and will not receive any new builds
%hr
-%p.lead To start serving your builds you can either add specific runners to your project or use shared runners
+%p.lead To start serving your builds you can either add specific Runners to your project or use shared Runners
.row
.col-sm-6
= render 'specific_runners'
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 9adce776c1c..ea4deb6cb28 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -71,9 +71,8 @@
= render 'shared/members/access_request_buttons', source: @project
= render "projects/buttons/koding"
- .btn-group.project-repo-btn-group
- = render 'projects/buttons/download', project: @project, ref: @ref
- = render 'projects/buttons/dropdown'
+ = render 'projects/buttons/download', project: @project, ref: @ref
+ = render 'projects/buttons/dropdown'
= render 'shared/notifications/button', notification_setting: @notification_setting
- if @repository.commit
diff --git a/app/views/projects/snippets/_actions.html.haml b/app/views/projects/snippets/_actions.html.haml
index 4aa4ab46a2f..32e1f8a21b0 100644
--- a/app/views/projects/snippets/_actions.html.haml
+++ b/app/views/projects/snippets/_actions.html.haml
@@ -1,7 +1,7 @@
.hidden-xs
- if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New Snippet" do
- New Snippet
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: 'btn btn-grouped btn-create new-snippet-link', title: "New snippet" do
+ New snippet
- if can?(current_user, :update_project_snippet, @snippet)
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
@@ -12,13 +12,13 @@
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
- if can?(current_user, :create_project_snippet, @project)
%li
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New Snippet" do
- New Snippet
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), title: "New snippet" do
+ New snippet
- if can?(current_user, :update_project_snippet, @snippet)
%li
= link_to namespace_project_snippet_path(@project.namespace, @project, @snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
diff --git a/app/views/projects/snippets/index.html.haml b/app/views/projects/snippets/index.html.haml
index 1646bcf4b8a..e77e1b026f6 100644
--- a/app/views/projects/snippets/index.html.haml
+++ b/app/views/projects/snippets/index.html.haml
@@ -1,10 +1,9 @@
- page_title "Snippets"
.sub-header-block
- .pull-right
- - if can?(current_user, :create_project_snippet, @project)
- = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new", title: "New Snippet" do
- New Snippet
+ - if can?(current_user, :create_project_snippet, @project)
+ = link_to new_namespace_project_snippet_path(@project.namespace, @project), class: "btn btn-new btn-wide-on-sm pull-right", title: "New snippet" do
+ New snippet
.oneline
Share code pastes with others out of git repository
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index a156d98bab8..05fccb4f976 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -30,4 +30,4 @@
.description.prepend-top-default
.wiki
= preserve do
- = markdown release.description
+ = markdown_field(release, :description)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 6adbe9351dc..7a0d9dcc94f 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -14,7 +14,7 @@
%button.dropdown-toggle.btn{ type: 'button', data: { toggle: 'dropdown'} }
%span.light
= @sort.humanize
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
%li
= link_to filter_tags_path(sort: nil) do
diff --git a/app/views/projects/tags/show.html.haml b/app/views/projects/tags/show.html.haml
index 4dd7439b2d0..155af755759 100644
--- a/app/views/projects/tags/show.html.haml
+++ b/app/views/projects/tags/show.html.haml
@@ -33,6 +33,6 @@
.description
.wiki
= preserve do
- = markdown @release.description
+ = markdown_field(@release, :description)
- else
This tag has no release notes.
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 37d341212af..9864be3562a 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -4,8 +4,8 @@
= content_for :meta_tags do
- if current_user
= auto_discovery_link_tag(:atom, namespace_project_commits_url(@project.namespace, @project, @ref, format: :atom, private_token: current_user.private_token), title: "#{@project.name}:#{@ref} commits")
-= render 'projects/last_push'
= render "projects/commits/head"
+= render 'projects/last_push'
%div{ class: container_class }
.tree-controls
diff --git a/app/views/projects/wikis/_form.html.haml b/app/views/projects/wikis/_form.html.haml
index 643f7c589e6..6624d5cb427 100644
--- a/app/views/projects/wikis/_form.html.haml
+++ b/app/views/projects/wikis/_form.html.haml
@@ -24,7 +24,7 @@
= succeed '.' do
More examples are in the
- = link_to 'documentation', help_page_path("user/project/markdown", anchor: "wiki-specific-markdown")
+ = link_to 'documentation', help_page_path("user/markdown", anchor: "wiki-specific-markdown")
.form-group
= f.label :commit_message, class: 'control-label'
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 551a20c1044..09c4411d67e 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,15 +1,16 @@
-.scrolling-tabs-container.sub-nav-scroll
- = render 'shared/nav_scroll'
- .nav-links.sub-nav.scrolling-tabs
- %ul{ class: (container_class) }
- = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
- = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
+= content_for :sub_nav do
+ .scrolling-tabs-container.sub-nav-scroll
+ = render 'shared/nav_scroll'
+ .nav-links.sub-nav.scrolling-tabs
+ %ul{ class: (container_class) }
+ = nav_link(html_options: {class: params[:id] == 'home' ? 'active' : '' }) do
+ = link_to 'Home', namespace_project_wiki_path(@project.namespace, @project, :home)
- = nav_link(path: 'wikis#pages') do
- = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
+ = nav_link(path: 'wikis#pages') do
+ = link_to 'Pages', namespace_project_wiki_pages_path(@project.namespace, @project)
- = nav_link(path: 'wikis#git_access') do
- = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
- Git Access
+ = nav_link(path: 'wikis#git_access') do
+ = link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
+ Git Access
- = render 'projects/wikis/new'
+ = render 'projects/wikis/new'
diff --git a/app/views/search/results/_issue.html.haml b/app/views/search/results/_issue.html.haml
index 8f68d6d1b87..e010f21de5a 100644
--- a/app/views/search/results/_issue.html.haml
+++ b/app/views/search/results/_issue.html.haml
@@ -7,7 +7,7 @@
- if issue.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(truncate(issue.description, length: 200, separator: " "), { project: issue.project, author: issue.author }))
+ = search_md_sanitize(issue, :description)
%span.light
#{issue.project.name_with_namespace}
- if issue.closed?
diff --git a/app/views/search/results/_merge_request.html.haml b/app/views/search/results/_merge_request.html.haml
index 6331c2bd6b0..07b17bc69c0 100644
--- a/app/views/search/results/_merge_request.html.haml
+++ b/app/views/search/results/_merge_request.html.haml
@@ -6,7 +6,7 @@
- if merge_request.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(merge_request.description, { project: merge_request.project, author: merge_request.author }))
+ = search_md_sanitize(merge_request, :description)
%span.light
#{merge_request.project.name_with_namespace}
.pull-right
diff --git a/app/views/search/results/_milestone.html.haml b/app/views/search/results/_milestone.html.haml
index b31595d8d1c..9664f65a36e 100644
--- a/app/views/search/results/_milestone.html.haml
+++ b/app/views/search/results/_milestone.html.haml
@@ -6,4 +6,4 @@
- if milestone.description.present?
.description.term
= preserve do
- = search_md_sanitize(markdown(milestone.description))
+ = search_md_sanitize(milestone, :description)
diff --git a/app/views/search/results/_note.html.haml b/app/views/search/results/_note.html.haml
index e0400083870..f3701b89bb4 100644
--- a/app/views/search/results/_note.html.haml
+++ b/app/views/search/results/_note.html.haml
@@ -23,4 +23,4 @@
.note-search-result
.term
= preserve do
- = search_md_sanitize(markdown(note.note, {no_header_anchors: true, author: note.author}))
+ = search_md_sanitize(note, :note)
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 8824bcc158e..3480800369a 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,4 +1,5 @@
%ul.nav-links.event-filter.scrolling-tabs
+ = event_filter_link EventFilter.all, 'All'
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
diff --git a/app/views/shared/_label_row.html.haml b/app/views/shared/_label_row.html.haml
index 77676454b57..6f593e8dff9 100644
--- a/app/views/shared/_label_row.html.haml
+++ b/app/views/shared/_label_row.html.haml
@@ -12,4 +12,4 @@
= link_to_label(label, tooltip: false)
- if label.description
%span.label-description
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index 51622931e24..fbbf6f358c5 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -3,7 +3,7 @@
= project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }
%a.btn.btn-new.new-project-item-select-button
= local_assigns[:label]
- %b.caret
+ = icon('caret-down')
:javascript
$('.new-project-item-select-button').on('click', function() {
diff --git a/app/views/shared/_sort_dropdown.html.haml b/app/views/shared/_sort_dropdown.html.haml
index 249bce926ce..68e05cb72e1 100644
--- a/app/views/shared/_sort_dropdown.html.haml
+++ b/app/views/shared/_sort_dropdown.html.haml
@@ -5,29 +5,29 @@
= sort_options_hash[@sort]
- else
= sort_title_recently_created
- %b.caret
+ = icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right.dropdown-menu-sort
%li
- = link_to page_filter_path(sort: sort_value_priority) do
+ = link_to page_filter_path(sort: sort_value_priority, label: true) do
= sort_title_priority
- = link_to page_filter_path(sort: sort_value_recently_created) do
+ = link_to page_filter_path(sort: sort_value_recently_created, label: true) do
= sort_title_recently_created
- = link_to page_filter_path(sort: sort_value_oldest_created) do
+ = link_to page_filter_path(sort: sort_value_oldest_created, label: true) do
= sort_title_oldest_created
- = link_to page_filter_path(sort: sort_value_recently_updated) do
+ = link_to page_filter_path(sort: sort_value_recently_updated, label: true) do
= sort_title_recently_updated
- = link_to page_filter_path(sort: sort_value_oldest_updated) do
+ = link_to page_filter_path(sort: sort_value_oldest_updated, label: true) do
= sort_title_oldest_updated
- = link_to page_filter_path(sort: sort_value_milestone_soon) do
+ = link_to page_filter_path(sort: sort_value_milestone_soon, label: true) do
= sort_title_milestone_soon
- = link_to page_filter_path(sort: sort_value_milestone_later) do
+ = link_to page_filter_path(sort: sort_value_milestone_later, label: true) do
= sort_title_milestone_later
- if controller.controller_name == 'issues' || controller.action_name == 'issues'
- = link_to page_filter_path(sort: sort_value_due_date_soon) do
+ = link_to page_filter_path(sort: sort_value_due_date_soon, label: true) do
= sort_title_due_date_soon
- = link_to page_filter_path(sort: sort_value_due_date_later) do
+ = link_to page_filter_path(sort: sort_value_due_date_later, label: true) do
= sort_title_due_date_later
- = link_to page_filter_path(sort: sort_value_upvotes) do
+ = link_to page_filter_path(sort: sort_value_upvotes, label: true) do
= sort_title_upvotes
- = link_to page_filter_path(sort: sort_value_downvotes) do
+ = link_to page_filter_path(sort: sort_value_downvotes, label: true) do
= sort_title_downvotes
diff --git a/app/views/shared/_visibility_level.html.haml b/app/views/shared/_visibility_level.html.haml
index add4536a0a2..b11257ee0e6 100644
--- a/app/views/shared/_visibility_level.html.haml
+++ b/app/views/shared/_visibility_level.html.haml
@@ -6,7 +6,7 @@
- if can_change_visibility_level
= render('shared/visibility_radios', model_method: :visibility_level, form: f, selected_level: visibility_level, form_model: form_model)
- else
- .col-sm-10
+ %div
%span.info
= visibility_level_icon(visibility_level)
%strong
diff --git a/app/views/shared/_visibility_radios.html.haml b/app/views/shared/_visibility_radios.html.haml
index ebe2eb0433d..182c4eebd50 100644
--- a/app/views/shared/_visibility_radios.html.haml
+++ b/app/views/shared/_visibility_radios.html.haml
@@ -10,6 +10,6 @@
.option-descr
= visibility_level_description(level, form_model)
- unless restricted_visibility_levels.empty?
- .col-sm-10
+ %div
%span.info
Some visibility level settings have been restricted by the administrator.
diff --git a/app/views/shared/groups/_group.html.haml b/app/views/shared/groups/_group.html.haml
index 1ad95351005..dc4ee3074d2 100644
--- a/app/views/shared/groups/_group.html.haml
+++ b/app/views/shared/groups/_group.html.haml
@@ -35,4 +35,4 @@
- if group.description.present?
.description
- = markdown(group.description, pipeline: :description)
+ = markdown_field(group, :description)
diff --git a/app/views/shared/icons/_icon_empty_groups.svg b/app/views/shared/icons/_icon_empty_groups.svg
new file mode 100644
index 00000000000..9228be05f03
--- /dev/null
+++ b/app/views/shared/icons/_icon_empty_groups.svg
@@ -0,0 +1 @@
+<svg width="249" height="368" viewBox="891 156 249 368" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><rect id="a" width="131" height="162" rx="10"/><mask id="e" x="0" y="0" width="131" height="162" fill="#fff"><use xlink:href="#a"/></mask><path d="M223.616 127.958V108.96c0-4.416-3.584-8-8.005-8h-23.985c-2.778 0-5.98 2.014-7.18 4.5l-5.07 10.5h-49.763c-5.527 0-9.996 4.475-9.996 9.997v53.005c0 5.513 4.475 9.997 9.996 9.997h84.01c5.525 0 9.994-4.477 9.994-9.998v-51.004z" id="b"/><mask id="f" x="0" y="0" width="104" height="88" fill="#fff"><use xlink:href="#b"/></mask><path d="M47 25h.996C53.52 25 58 29.472 58 34.99v20.02C58 60.526 53.52 65 47.996 65H10.004C4.48 65 0 60.528 0 55.01V34.99C0 29.474 4.48 25 10.004 25H11v-7c0-9.94 8.06-18 18-18s18 8.06 18 18v7zm-6 0H17v-7c0-6.627 5.373-12 12-12s12 5.373 12 12v7z" id="c"/><mask id="g" x="0" y="0" width="58" height="65" fill="#fff"><use xlink:href="#c"/></mask><path d="M0 10.008C0 4.48 4.476 0 10 0h218c5.523 0 10 4.473 10 10.008v140.94c0 5.53-4.062 11.882-9.08 14.196l-100.84 46.5c-5.015 2.31-13.142 2.312-18.16 0l-100.84-46.5C4.064 162.832 0 156.484 0 150.95V10.007z" id="d"/><mask id="h" x="0" y="0" width="238" height="213.417" fill="#fff"><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(891 156)"><g transform="rotate(8 -266.528 490.3)"><use stroke="#E5E5E5" mask="url(#e)" stroke-width="8" fill="#FFF" xlink:href="#a"/><rect fill="#FC8A51" x="20" y="31" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="60" y="31" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="36" y="31" width="20" height="4" rx="2"/><rect fill="#6B4FBB" x="20" y="65" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="44" y="65" width="20" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="80" width="20" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="80" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="20" y="48" width="12" height="4" rx="2"/><rect fill="#FC8A51" x="36" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="60" y="80" width="12" height="4" rx="2"/><rect fill="#6B4FBB" x="52" y="48" width="12" height="4" rx="2"/><rect fill="#FDE5D8" x="68" y="48" width="12" height="4" rx="2"/></g><use stroke="#B5A7DD" mask="url(#f)" stroke-width="8" fill="#FFF" transform="rotate(5 171.616 144.96)" xlink:href="#b"/><path d="M58 132c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#C1E7D0"/><path d="M90.143 132c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M74.686 133.875l-3.18-3.18c-.29-.29-.77-.296-1.06-.005l-1.55 1.55c-.287.287-.29.766.004 1.06l4.92 4.92c.504.504 1.32.504 1.823 0l.654-.653 7.804-7.804c.3-.3.29-.77-.005-1.067l-1.578-1.58c-.302-.3-.775-.298-1.068-.004l-6.764 6.763z" fill="#31AF64"/><path d="M4 66c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18S4 75.94 4 66z" fill="#D5ECF7"/><path d="M36.143 66c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M22 55.714c5.68 0 10.286 4.605 10.286 10.286 0 5.68-4.605 10.286-10.286 10.286-3.45 0-6.505-1.7-8.37-4.307L22 66V55.714z" fill="#2D9FD8"/><g transform="rotate(-8 748.533 18.147)"><use stroke="#FDE5D8" mask="url(#g)" stroke-width="8" fill="#FFF" xlink:href="#c"/><path d="M31 46.584c1.766-.772 3-2.534 3-4.584 0-2.76-2.24-5-5-5s-5 2.24-5 5c0 2.05 1.234 3.812 3 4.584v3.42c0 1.1.895 1.996 2 1.996 1.112 0 2-.894 2-1.997v-3.42z" fill="#FC8A51"/></g><g transform="translate(0 154)"><use stroke="#E5E5E5" mask="url(#h)" stroke-width="8" fill="#FFF" xlink:href="#d"/><g opacity=".3"><path d="M141.837 104.53l-2.56-7.993-5.074-15.843c-.26-.815-1.398-.815-1.66 0l-5.074 15.843h-16.85l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33 22.16-16.33c.61-.452.866-1.25.632-1.98" fill="#A1A1A1"/><path fill="#5C5C5C" d="M119.044 122.84l8.425-26.303h-16.85l8.424 26.304"/><path fill="#787878" d="M119.044 122.84l-8.425-26.303H98.81l20.232 26.304"/><path fill="#787878" d="M119.044 122.84l8.425-26.303h11.807l-20.233 26.304"/><path d="M98.812 96.537l-2.56 7.993c-.234.73.022 1.528.633 1.98l22.16 16.33L98.81 96.538z" fill="#A1A1A1"/><path d="M98.812 96.537h11.807l-5.075-15.843c-.26-.815-1.398-.815-1.66 0l-5.073 15.843z" fill="#5C5C5C"/><path d="M139.277 96.537l2.56 7.993c.234.73-.022 1.528-.634 1.98l-22.16 16.33 20.234-26.303z" fill="#A1A1A1"/><path d="M139.277 96.537H127.47l5.074-15.843c.26-.815 1.398-.815 1.66 0l5.073 15.843z" fill="#5C5C5C"/></g><path d="M57 18.29c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H41c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83H77c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm36 0c1.105 0 2-.818 2-1.828 0-1.01-.895-1.83-2-1.83h-16c-1.105 0-2 .82-2 1.83 0 1.01.895 1.83 2 1.83h16zm17 24.693c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V28.35c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.633zm202 32.923c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V61.274c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm202 32.923c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm-202 0c0 1.01.895 1.828 2 1.828s2-.82 2-1.83V94.2c0-1.012-.895-1.83-2-1.83s-2 .818-2 1.83v14.63zm202 32.922c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm-202 0c0 1.01.895 1.83 2 1.83s2-.82 2-1.83V127.12c0-1.01-.895-1.83-2-1.83s-2 .82-2 1.83v14.632zm179.023 19.555c-.988.452-1.388 1.55-.894 2.454.493.904 1.694 1.27 2.682.82l14.31-6.545c.99-.452 1.39-1.55.896-2.454-.494-.902-1.696-1.27-2.684-.817l-14.31 6.544zm-32.2 14.723c-.987.452-1.388 1.55-.894 2.454.493.904 1.695 1.27 2.683.818l14.31-6.544c.99-.45 1.39-1.55.895-2.454-.494-.903-1.695-1.27-2.683-.818l-14.31 6.544zm-32.2 14.724c-.987.45-1.387 1.55-.893 2.454.494.903 1.695 1.27 2.683.818l14.31-6.544c.99-.452 1.39-1.55.896-2.454-.495-.904-1.697-1.27-2.685-.818l-14.31 6.544zm-23.67-2.023l-12.186-5.57c-.987-.452-2.19-.086-2.683.817-.494.904-.093 2.003.895 2.454l12.185 5.573c.754.345 1.57.645 2.438.898 1.052.307 2.177-.224 2.513-1.187.335-.962-.246-1.99-1.298-2.298-.677-.197-1.302-.426-1.864-.684zM62.57 168.437c-.988-.452-2.19-.086-2.683.818-.494.903-.094 2.002.894 2.454l14.31 6.544c.988.45 2.19.085 2.683-.818.494-.904.094-2.003-.894-2.454l-14.312-6.544zm-32.2-14.723c-.988-.452-2.19-.086-2.683.818-.494.904-.093 2.003.895 2.454l14.31 6.544c.988.452 2.19.086 2.684-.818.494-.903.093-2.002-.895-2.454l-14.312-6.543z" fill="#EEE"/></g><g><path d="M104 18c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#FADFD9"/><path d="M136.143 18c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M119.43 8.994c0-.707.57-1.28 1.283-1.28h2.574c.71 0 1.284.57 1.284 1.28v10.298c0 .706-.57 1.28-1.283 1.28h-2.574c-.71 0-1.284-.57-1.284-1.28V8.994zm0 15.433c0-.71.57-1.284 1.283-1.284h2.574c.71 0 1.284.57 1.284 1.284V27c0 .71-.57 1.286-1.283 1.286h-2.574c-.71 0-1.284-.57-1.284-1.285v-2.573z" fill="#E75E40"/></g><g><path d="M213 89c0-9.94 8.06-18 18-18s18 8.06 18 18-8.06 18-18 18-18-8.06-18-18z" fill="#F6D4DC"/><path d="M245.143 89c0-7.81-6.332-14.143-14.143-14.143-7.81 0-14.143 6.332-14.143 14.143 0 7.81 6.332 14.143 14.143 14.143 7.81 0 14.143-6.332 14.143-14.143z" fill="#FFF"/><path d="M231 86.348l-3.603-3.602c-.288-.29-.766-.286-1.063.01l-1.578 1.578c-.3.302-.3.773-.01 1.063L228.348 89l-3.602 3.603c-.29.288-.286.766.01 1.063l1.578 1.578c.302.3.773.3 1.063.01L231 91.652l3.603 3.602c.288.29.766.286 1.063-.01l1.578-1.578c.3-.302.3-.773.01-1.063L233.652 89l3.602-3.603c.29-.288.286-.766-.01-1.063l-1.578-1.578c-.302-.3-.773-.3-1.063-.01L231 86.348z" fill="#D22852"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/icons/_icon_no_wrap.svg b/app/views/shared/icons/_icon_no_wrap.svg
new file mode 100644
index 00000000000..fe34cada002
--- /dev/null
+++ b/app/views/shared/icons/_icon_no_wrap.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="m6 11h-4.509c-.263 0-.491.226-.491.505v.991c0 .291.22.505.491.505h4.509v.679c0 .301.194.413.454.236l2.355-1.607c.251-.171.259-.442 0-.619l-2.355-1.607c-.251-.171-.454-.07-.454.236v.681m-5-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m10 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991m-10-4c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991"/>
+</svg>
diff --git a/app/views/shared/icons/_icon_soft_wrap.svg b/app/views/shared/icons/_icon_soft_wrap.svg
new file mode 100644
index 00000000000..ea27a2024b1
--- /dev/null
+++ b/app/views/shared/icons/_icon_soft_wrap.svg
@@ -0,0 +1,3 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16">
+ <path fill-rule="evenodd" d="m12 11h-2v-.681c0-.307-.203-.407-.454-.236l-2.355 1.607c-.259.177-.251.448 0 .619l2.355 1.607c.259.177.454.065.454-.236v-.679h2c0 0 0 0 0 0 1.657 0 3-1.343 3-3 0-.828-.336-1.578-.879-2.121-.543-.543-1.293-.879-2.121-.879-.001 0-.002 0-.002 0h-10.497c-.271 0-.5.226-.5.505v.991c0 .291.224.505.5.505h10.497c.001 0 .002 0 .002 0 .552 0 1 .448 1 1 0 .276-.112.526-.293.707-.181.181-.431.293-.707.293m-11-7.495c0-.279.22-.505.498-.505h13c.275 0 .498.214.498.505v.991c0 .279-.22.505-.498.505h-13c-.275 0-.498-.214-.498-.505v-.991m0 8c0-.279.215-.505.49-.505h3.02c.271 0 .49.214.49.505v.991c0 .279-.215.505-.49.505h-3.02c-.271 0-.49-.214-.49-.505v-.991"/>
+</svg>
diff --git a/app/views/shared/icons/_illustration_no_commits.svg b/app/views/shared/icons/_illustration_no_commits.svg
new file mode 100644
index 00000000000..4f9d9add60d
--- /dev/null
+++ b/app/views/shared/icons/_illustration_no_commits.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 168 107" xmlns:xlink="http://www.w3.org/1999/xlink"><g fill="#eee" fill-rule="evenodd"><path d="m4.01 2h1.102c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-1.102c-2.218 0-4.01 1.788-4.01 4 0 .552.448 1 1 1 .552 0 1-.448 1-1 0-1.108.892-2 2.01-2m12.702 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m11.6 0c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7m8.088 0c.822 0 1.554.503 1.86 1.254.208.512.791.758 1.303.55.512-.208.758-.791.55-1.303-.609-1.497-2.069-2.5-3.712-2.5h-2.188c-.552 0-1 .448-1 1 0 .552.448 1 1 1h2.188m2.01 12.518c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 11.6c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7m0 6.282c0 1.108-.892 2-2.01 2h-.72c-.552 0-1 .448-1 1 0 .552.448 1 1 1h.72c2.218 0 4.01-1.788 4.01-4v-.382c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.382m-14.325 2c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-11.6 0c-.552 0-1 .448-1 1 0 .552.448 1 1 1h5.7c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-5.7m-8.47 0c-.755 0-1.438-.424-1.782-1.085-.255-.49-.859-.681-1.349-.426-.49.255-.681.859-.426 1.349.684 1.316 2.046 2.162 3.556 2.162h2.57c.552 0 1-.448 1-1 0-.552-.448-1-1-1h-2.57m-2.01-12.136c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-11.6c0-.552-.448-1-1-1-.552 0-1 .448-1 1v5.7c0 .552.448 1 1 1 .552 0 1-.448 1-1v-5.7m0-6.664c0-.552-.448-1-1-1-.552 0-1 .448-1 1v.764c0 .552.448 1 1 1 .552 0 1-.448 1-1v-.764" id="0"/><circle cx="21" cy="24" r="10"/><rect width="33" height="3" x="37" y="18" rx="1.5" id="1"/><rect width="53" height="3" x="37" y="27" rx="1.5" id="2"/><path d="m131 29c0 .552.447.999.996.999h22.01c.545 0 .996-.451.996-.999v-9c0-.552-.447-.999-.996-.999h-22.01c-.545 0-.996.451-.996.999v9m.996-12h22.01c1.655 0 2.996 1.344 2.996 2.999v9c0 1.657-1.35 2.999-2.996 2.999h-22.01c-1.655 0-2.996-1.344-2.996-2.999v-9c0-1.657 1.35-2.999 2.996-2.999" id="3"/><g transform="translate(0 59)"><use xlink:href="#0"/><circle cx="21" cy="24" r="10"/><use xlink:href="#1"/><use xlink:href="#2"/><use xlink:href="#3"/></g></g></svg> \ No newline at end of file
diff --git a/app/views/shared/issuable/_filter.html.haml b/app/views/shared/issuable/_filter.html.haml
index cf26197f7d7..31620297be0 100644
--- a/app/views/shared/issuable/_filter.html.haml
+++ b/app/views/shared/issuable/_filter.html.haml
@@ -1,3 +1,4 @@
+- finder = controller.controller_name == 'issues' || controller.controller_name == 'boards' ? issues_finder : merge_requests_finder
- boards_page = controller.controller_name == 'boards'
.issues-filters
@@ -14,19 +15,19 @@
- if params[:author_id].present?
= hidden_field_tag(:author_id, params[:author_id])
= dropdown_tag(user_dropdown_label(params[:author_id], "Author"), options: { toggle_class: "js-user-search js-filter-submit js-author-search", title: "Filter by author", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-author js-filter-submit",
- placeholder: "Search authors", data: { any_user: "Any Author", first_user: (current_user.username if current_user), current_user: true, project_id: (@project.id if @project), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
+ placeholder: "Search authors", data: { any_user: "Any Author", first_user: current_user.try(:username), current_user: true, project_id: @project.try(:id), selected: params[:author_id], field_name: "author_id", default_label: "Author" } })
.filter-item.inline
- if params[:assignee_id].present?
= hidden_field_tag(:assignee_id, params[:assignee_id])
= dropdown_tag(user_dropdown_label(params[:assignee_id], "Assignee"), options: { toggle_class: "js-user-search js-filter-submit js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
- placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: (current_user.username if current_user), null_user: true, current_user: true, project_id: (@project.id if @project), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
+ placeholder: "Search assignee", data: { any_user: "Any Assignee", first_user: current_user.try(:username), null_user: true, current_user: true, project_id: @project.try(:id), selected: params[:assignee_id], field_name: "assignee_id", default_label: "Assignee" } })
.filter-item.inline.milestone-filter
- = render "shared/issuable/milestone_dropdown"
+ = render "shared/issuable/milestone_dropdown", selected: finder.milestones.try(:first), name: :milestone_title, show_any: true, show_upcoming: true
.filter-item.inline.labels-filter
- = render "shared/issuable/label_dropdown"
+ = render "shared/issuable/label_dropdown", selected: finder.labels.select(:title).uniq, use_id: false, selected_toggle: params[:label_name], data_options: { field_name: "label_name[]" }
.filter-item.inline.reset-filters
%a{href: page_filter_path(without: [:assignee_id, :author_id, :milestone_title, :label_name, :search])} Reset filters
@@ -37,7 +38,7 @@
%input.pull-left.form-control{ type: "search", placeholder: "Filter by name...", "v-model" => "filters.search", "debounce" => "250" }
- if can?(current_user, :admin_list, @project)
.dropdown.pull-right
- %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, project_id: @project.try(:id) } }
+ %button.btn.btn-create.js-new-board-list{ type: "button", data: { toggle: "dropdown", labels: labels_filter_path, namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path) } }
Create new list
.dropdown-menu.dropdown-menu-paging.dropdown-menu-align-right.dropdown-menu-issues-board-new.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { show_footer: true, show_create: true, show_boards_content: true, title: "Create a new list" }
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 04373684ee9..c3f4e10c954 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -1,3 +1,4 @@
+- project = @target_project || @project
= form_errors(issuable)
- if @conflict
@@ -82,38 +83,22 @@
= f.label :assignee_id, "Assignee", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
.issuable-form-select-holder
- = users_select_tag("#{issuable.class.model_name.param_key}[assignee_id]",
- placeholder: 'Select assignee', class: 'custom-form-control', null_user: true,
- selected: issuable.assignee_id, project: @target_project || @project,
- first_user: true, current_user: true, include_blank: true)
- %div
- = link_to 'Assign to me', '#', class: 'assign-to-me-link prepend-top-5 inline'
+ - if issuable.assignee_id
+ = f.hidden_field :assignee_id
+ = dropdown_tag(user_dropdown_label(issuable.assignee_id, "Assignee"), options: { toggle_class: "js-dropdown-keep-input js-user-search js-issuable-form-dropdown js-assignee-search", title: "Filter by assignee", filter: true, dropdown_class: "dropdown-menu-user dropdown-menu-selectable dropdown-menu-assignee js-filter-submit",
+ placeholder: "Search assignee", data: { first_user: current_user.try(:username), null_user: true, current_user: true, project_id: project.try(:id), selected: issuable.assignee_id, field_name: "#{issuable.class.model_name.param_key}[assignee_id]", default_label: "Assignee", show_menu_above: true } })
.form-group.issue-milestone
= f.label :milestone_id, "Milestone", class: "control-label #{"col-lg-4" if has_due_date}"
.col-sm-10{ class: ("col-lg-8" if has_due_date) }
- - if milestone_options(issuable).present?
- .issuable-form-select-holder
- = f.select(:milestone_id, milestone_options(issuable),
- { include_blank: true }, { class: 'select2', data: { placeholder: 'Select milestone' } })
- - else
- .prepend-top-10
- %span.light No open milestones available.
- - if can? current_user, :admin_milestone, issuable.project
- %div
- = link_to 'Create new milestone', new_namespace_project_milestone_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ .issuable-form-select-holder
+ = render "shared/issuable/milestone_dropdown", selected: issuable.milestone, name: "#{issuable.class.model_name.param_key}[milestone_id]", show_any: false, show_menu_above: true, show_upcoming: false, extra_class: "js-issuable-form-dropdown js-dropdown-keep-input"
.form-group
- has_labels = issuable.project.labels.any?
= f.label :label_ids, "Labels", class: "control-label #{"col-lg-4" if has_due_date}"
+ = f.hidden_field :label_ids, multiple: true, value: ''
.col-sm-10{ class: "#{"col-lg-8" if has_due_date} #{'issuable-form-padding-top' if !has_labels}" }
- - if has_labels
- .issuable-form-select-holder
- = f.collection_select :label_ids, issuable.project.labels.all, :id, :name,
- { selected: issuable.label_ids }, multiple: true, class: 'select2', data: { placeholder: "Select labels" }
- - else
- %span.light No labels yet.
- - if can? current_user, :admin_label, issuable.project
- %div
- = link_to 'Create new label', new_namespace_project_label_path(issuable.project.namespace, issuable.project), target: :blank, class: "prepend-top-5 inline"
+ .issuable-form-select-holder
+ = render "shared/issuable/label_dropdown", classes: ["js-issuable-form-dropdown"], selected: issuable.labels, data_options: { field_name: "#{issuable.class.model_name.param_key}[label_ids][]", show_any: false, show_menu_above: 'true' }
- if has_due_date
.col-lg-6
.form-group
diff --git a/app/views/shared/issuable/_label_dropdown.html.haml b/app/views/shared/issuable/_label_dropdown.html.haml
index d34d28f6736..6d307611640 100644
--- a/app/views/shared/issuable/_label_dropdown.html.haml
+++ b/app/views/shared/issuable/_label_dropdown.html.haml
@@ -1,25 +1,29 @@
+- project = @target_project || @project
- show_create = local_assigns.fetch(:show_create, true)
- extra_options = local_assigns.fetch(:extra_options, true)
- filter_submit = local_assigns.fetch(:filter_submit, true)
- show_footer = local_assigns.fetch(:show_footer, true)
+- use_id = local_assigns.fetch(:use_id, true)
- data_options = local_assigns.fetch(:data_options, {})
- classes = local_assigns.fetch(:classes, [])
-- dropdown_data = {toggle: 'dropdown', field_name: 'label_name[]', show_no: "true", show_any: "true", selected: params[:label_name], project_id: @project.try(:id), labels: labels_filter_path, default_label: "Label"}
+- selected = local_assigns.fetch(:selected, nil)
+- selected_toggle = local_assigns.fetch(:selected_toggle, nil)
+- dropdown_data = {toggle: 'dropdown', field_name: "label_name[]", show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), labels: labels_filter_path, default_label: "Labels"}
- dropdown_data.merge!(data_options)
- classes << 'js-extra-options' if extra_options
- classes << 'js-filter-submit' if filter_submit
-- if params[:label_name].present?
- - if params[:label_name].respond_to?('any?')
- - params[:label_name].each do |label|
- = hidden_field_tag "label_name[]", label, id: nil
+- if selected
+ - selected.each do |label|
+ = hidden_field_tag data_options[:field_name], use_id ? label.try(:id) : label.try(:title), id: nil
+
.dropdown
%button.dropdown-menu-toggle.js-label-select.js-multiselect{class: classes.join(' '), type: "button", data: dropdown_data}
- %span.dropdown-toggle-text
- = h(multi_label_name(params[:label_name], "Label"))
+ %span.dropdown-toggle-text{ class: ("is-default" if selected.nil? || selected.empty?) }
+ = multi_label_name(selected, "Labels")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default", locals: { title: "Filter by label", show_footer: show_footer, show_create: show_create }
- - if show_create and @project and can?(current_user, :admin_label, @project)
+ - if show_create && project && can?(current_user, :admin_label, project)
= render partial: "shared/issuable/label_page_create"
= dropdown_loading
diff --git a/app/views/shared/issuable/_milestone_dropdown.html.haml b/app/views/shared/issuable/_milestone_dropdown.html.haml
index 2fcf40ece99..ab3cc33d18f 100644
--- a/app/views/shared/issuable/_milestone_dropdown.html.haml
+++ b/app/views/shared/issuable/_milestone_dropdown.html.haml
@@ -1,16 +1,20 @@
-- if params[:milestone_title].present?
- = hidden_field_tag(:milestone_title, params[:milestone_title])
-= dropdown_tag(milestone_dropdown_label(params[:milestone_title]), options: { title: "Filter by milestone", toggle_class: 'js-milestone-select js-filter-submit', filter: true, dropdown_class: "dropdown-menu-selectable",
- placeholder: "Search milestones", footer_content: @project.present?, data: { show_no: true, show_any: true, show_upcoming: true, field_name: "milestone_title", selected: params[:milestone_title], project_id: @project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
- - if @project
+- project = @target_project || @project
+- extra_class = extra_class || ''
+- show_menu_above = show_menu_above || false
+- selected_text = selected.try(:title)
+- if selected.present?
+ = hidden_field_tag(name, name == :milestone_title ? selected.title : selected.id)
+= dropdown_tag(milestone_dropdown_label(selected_text), options: { title: "Filter by milestone", toggle_class: "js-milestone-select js-filter-submit #{extra_class}", filter: true, dropdown_class: "dropdown-menu-selectable dropdown-menu-milestone",
+ placeholder: "Search milestones", footer_content: project.present?, data: { show_no: true, show_menu_above: show_menu_above, show_any: show_any, show_upcoming: show_upcoming, field_name: name, selected: selected.try(:title), project_id: project.try(:id), milestones: milestones_filter_dropdown_path, default_label: "Milestone" } }) do
+ - if project
%ul.dropdown-footer-list
- - if can? current_user, :admin_milestone, @project
+ - if can? current_user, :admin_milestone, project
%li
- = link_to new_namespace_project_milestone_path(@project.namespace, @project), title: "New Milestone" do
+ = link_to new_namespace_project_milestone_path(project.namespace, project), title: "New Milestone" do
Create new
%li
- = link_to namespace_project_milestones_path(@project.namespace, @project) do
- - if can? current_user, :admin_milestone, @project
+ = link_to namespace_project_milestones_path(project.namespace, project) do
+ - if can? current_user, :admin_milestone, project
Manage milestones
- else
View milestones
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 1d9b09a5ef1..5527a2f889a 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -1,25 +1,25 @@
+- type = local_assigns.fetch(:type, :issues)
+- page_context_word = type.to_s.humanize(capitalize: false)
+- issuables = @issues || @merge_requests
+
%ul.nav-links.issues-state-filters
- - if defined?(type) && type == :merge_requests
- - page_context_word = 'merge requests'
- - else
- - page_context_word = 'issues'
%li{class: ("active" if params[:state] == 'opened')}
= link_to page_filter_path(state: 'opened', label: true), title: "Filter by #{page_context_word} that are currently opened." do
- #{state_filters_text_for(:opened, @project)}
+ #{issuables_state_counter_text(type, :opened)}
- - if defined?(type) && type == :merge_requests
+ - if type == :merge_requests
%li{class: ("active" if params[:state] == 'merged')}
= link_to page_filter_path(state: 'merged', label: true), title: 'Filter by merge requests that are currently merged.' do
- #{state_filters_text_for(:merged, @project)}
+ #{issuables_state_counter_text(type, :merged)}
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by merge requests that are currently closed and unmerged.' do
- #{state_filters_text_for(:closed, @project)}
+ #{issuables_state_counter_text(type, :closed)}
- else
%li{class: ("active" if params[:state] == 'closed')}
= link_to page_filter_path(state: 'closed', label: true), title: 'Filter by issues that are currently closed.' do
- #{state_filters_text_for(:closed, @project)}
+ #{issuables_state_counter_text(type, :closed)}
%li{class: ("active" if params[:state] == 'all')}
= link_to page_filter_path(state: 'all', label: true), title: "Show all #{page_context_word}." do
- #{state_filters_text_for(:all, @project)}
+ #{issuables_state_counter_text(type, :all)}
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index b13daaf43c9..f8059988038 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -108,29 +108,30 @@
.js-due-date-calendar
- if issuable.project.labels.any?
+ - selected_labels = issuable.labels
.block.labels
.sidebar-collapsed-icon.js-sidebar-labels-tooltip{ title: issuable_labels_tooltip(issuable.labels_array), data: { placement: "left", container: "body" } }
= icon('tags')
%span
- = issuable.labels_array.size
+ = selected_labels.size
.title.hide-collapsed
Labels
= icon('spinner spin', class: 'block-loading')
- if can_edit_issuable
= link_to 'Edit', '#', class: 'edit-link pull-right'
- .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if issuable.labels_array.any?) }
- - if issuable.labels_array.any?
- - issuable.labels_array.each do |label|
+ .value.issuable-show-labels.hide-collapsed{ class: ("has-labels" if selected_labels.any?) }
+ - if selected_labels.any?
+ - selected_labels.each do |label|
= link_to_label(label, type: issuable.to_ability_name)
- else
%span.no-value None
.selectbox.hide-collapsed
- - issuable.labels_array.each do |label|
+ - selected_labels.each do |label|
= hidden_field_tag "#{issuable.to_ability_name}[label_names][]", label.id, id: nil
.dropdown
- %button.dropdown-menu-toggle.js-label-select.js-multiselect{type: "button", data: {toggle: "dropdown", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", project_id: (@project.id if @project), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
- %span.dropdown-toggle-text
- Label
+ %button.dropdown-menu-toggle.js-label-select.js-multiselect.js-label-sidebar-dropdown{type: "button", data: {toggle: "dropdown", default_label: "Labels", field_name: "#{issuable.to_ability_name}[label_names][]", ability_name: issuable.to_ability_name, show_no: "true", show_any: "true", namespace_path: @project.try(:namespace).try(:path), project_path: @project.try(:path), issue_update: issuable_json_path(issuable), labels: (namespace_project_labels_path(@project.namespace, @project, :json) if @project)}}
+ %span.dropdown-toggle-text{ class: ("is-default" if selected_labels.empty?)}
+ = multi_label_name(selected_labels, "Labels")
= icon('chevron-down')
.dropdown-menu.dropdown-select.dropdown-menu-paging.dropdown-menu-labels.dropdown-menu-selectable
= render partial: "shared/issuable/label_page_default"
diff --git a/app/views/shared/milestones/_labels_tab.html.haml b/app/views/shared/milestones/_labels_tab.html.haml
index b15e8ea73fe..33f93dccd3c 100644
--- a/app/views/shared/milestones/_labels_tab.html.haml
+++ b/app/views/shared/milestones/_labels_tab.html.haml
@@ -8,7 +8,7 @@
= link_to milestones_label_path(options) do
- render_colored_label(label, tooltip: false)
%span.prepend-description-left
- = markdown(label.description, pipeline: :single_line)
+ = markdown_field(label, :description)
.pull-info-right
%span.append-right-20
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 7ff947a51db..548215243db 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -26,7 +26,7 @@
.detail-page-description.milestone-detail
%h2.title
- = markdown escape_once(milestone.title), pipeline: :single_line
+ = markdown_field(milestone, :title)
- if milestone.complete?(current_user) && milestone.active?
.alert.alert-success.prepend-top-default
@@ -55,4 +55,3 @@
Open
%td
= ms.expires_at
-
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
index ff1cf966a9b..feaa5570c21 100644
--- a/app/views/shared/notifications/_button.html.haml
+++ b/app/views/shared/notifications/_button.html.haml
@@ -11,7 +11,7 @@
= icon("bell", class: "js-notification-loading")
= notification_title(notification_setting.level)
%button.btn.dropdown-toggle{ data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
- %span.caret
+ = icon('caret-down')
.sr-only Toggle dropdown
- else
%button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "dropdown", target: notifications_menu_identifier("dropdown", notification_setting) } }
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 66c309644a7..e8668048703 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -50,4 +50,4 @@
class: "commit-row-message"
- elsif project.description.present?
.description
- = markdown(project.description, pipeline: :description)
+ = markdown_field(project, :description)
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 773ce8ac240..dcdba01aee9 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -1,9 +1,12 @@
- unless @snippet.content.empty?
- if markup?(@snippet.file_name)
%textarea.markdown-snippet-copy.blob-content{data: {blob_id: @snippet.id}}
- = @snippet.data
+ = @snippet.content
.file-content.wiki
- = render_markup(@snippet.file_name, @snippet.data)
+ - if gitlab_markdown?(@snippet.file_name)
+ = preserve(markdown_field(@snippet, :content))
+ - else
+ = render_markup(@snippet.file_name, @snippet.content)
- else
= render 'shared/file_highlight', blob: @snippet
- else
diff --git a/app/views/shared/snippets/_header.html.haml b/app/views/shared/snippets/_header.html.haml
index 7ae4211ddfd..d7506e07ff6 100644
--- a/app/views/shared/snippets/_header.html.haml
+++ b/app/views/shared/snippets/_header.html.haml
@@ -21,4 +21,4 @@
= render "snippets/actions"
%h2.snippet-title.prepend-top-0.append-bottom-0
- = markdown escape_once(@snippet.title), pipeline: :single_line, author: @snippet.author
+ = markdown_field(@snippet, :title)
diff --git a/app/views/snippets/_actions.html.haml b/app/views/snippets/_actions.html.haml
index fdaca199218..1d0e549ed3d 100644
--- a/app/views/snippets/_actions.html.haml
+++ b/app/views/snippets/_actions.html.haml
@@ -1,7 +1,7 @@
.hidden-xs
- if current_user
- = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New Snippet" do
- New Snippet
+ = link_to new_snippet_path, class: "btn btn-grouped btn-create new-snippet-link", title: "New snippet" do
+ New snippet
- if can?(current_user, :admin_personal_snippet, @snippet)
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, class: "btn btn-grouped btn-danger", title: 'Delete Snippet' do
Delete
@@ -12,12 +12,12 @@
.visible-xs-block.dropdown
%button.btn.btn-default.btn-block.append-bottom-0.prepend-top-5{ data: { toggle: "dropdown" } }
Options
- %span.caret
+ = icon('caret-down')
.dropdown-menu.dropdown-menu-full-width
%ul
%li
- = link_to new_snippet_path, title: "New Snippet" do
- New Snippet
+ = link_to new_snippet_path, title: "New snippet" do
+ New snippet
- if can?(current_user, :admin_personal_snippet, @snippet)
%li
= link_to snippet_path(@snippet), method: :delete, data: { confirm: "Are you sure?" }, title: 'Delete Snippet' do
diff --git a/app/views/snippets/_snippets.html.haml b/app/views/snippets/_snippets.html.haml
index 7be4a471579..77b66ca74b6 100644
--- a/app/views/snippets/_snippets.html.haml
+++ b/app/views/snippets/_snippets.html.haml
@@ -1,3 +1,5 @@
+- remote = local_assigns.fetch(:remote, false)
+
.snippets-list-holder
%ul.content-list
= render partial: 'shared/snippets/snippet', collection: @snippets
@@ -5,7 +7,7 @@
%li
.nothing-here-block Nothing here.
- = paginate @snippets, theme: 'gitlab', remote: true
+ = paginate @snippets, theme: 'gitlab', remote: remote
:javascript
gl.SnippetsList();
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 2a57ac90bab..1e0752bd3c3 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -36,15 +36,15 @@
.avatar-holder
= link_to avatar_icon(@user, 400), target: '_blank' do
= image_tag avatar_icon(@user, 90), class: "avatar s90", alt: ''
-
+
.user-info
.cover-title
= @user.name
- %span.handle
- @#{@user.username}
.cover-desc.member-date
%span.middle-dot-divider
+ @#{@user.username}
+ %span.middle-dot-divider
Member since #{@user.created_at.to_s(:medium)}
.cover-desc
@@ -70,6 +70,10 @@
.profile-link-holder.middle-dot-divider
= icon('map-marker')
= @user.location
+ - unless @user.organization.blank?
+ .profile-link-holder.middle-dot-divider
+ = icon('building')
+ = @user.organization
- if @user.bio.present?
.cover-desc
@@ -78,7 +82,7 @@
%ul.nav-links.center.user-profile-nav
%li.js-activity-tab
- = link_to user_calendar_activities_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
+ = link_to user_path, data: {target: 'div#activity', action: 'activity', toggle: 'tab'} do
Activity
%li.js-groups-tab
= link_to user_groups_path, data: {target: 'div#groups', action: 'groups', toggle: 'tab'} do
diff --git a/app/workers/clear_database_cache_worker.rb b/app/workers/clear_database_cache_worker.rb
new file mode 100644
index 00000000000..c541daba50e
--- /dev/null
+++ b/app/workers/clear_database_cache_worker.rb
@@ -0,0 +1,23 @@
+# This worker clears all cache fields in the database, working in batches.
+class ClearDatabaseCacheWorker
+ include Sidekiq::Worker
+
+ BATCH_SIZE = 1000
+
+ def perform
+ CacheMarkdownField.caching_classes.each do |kls|
+ fields = kls.cached_markdown_fields.html_fields
+ clear_cache_fields = fields.each_with_object({}) do |field, memo|
+ memo[field] = nil
+ end
+
+ Rails.logger.debug("Clearing Markdown cache for #{kls}: #{fields.inspect}")
+
+ kls.unscoped.in_batches(of: BATCH_SIZE) do |relation|
+ relation.update_all(clear_cache_fields)
+ end
+ end
+
+ nil
+ end
+end
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index c64ea108d52..174eabff9fd 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -2,12 +2,11 @@ class ExpireBuildArtifactsWorker
include Sidekiq::Worker
def perform
- Rails.logger.info 'Cleaning old build artifacts'
+ Rails.logger.info 'Scheduling removal of build artifacts'
- builds = Ci::Build.with_expired_artifacts
- builds.find_each(batch_size: 50).each do |build|
- Rails.logger.debug "Removing artifacts build #{build.id}..."
- build.erase_artifacts!
- end
+ build_ids = Ci::Build.with_expired_artifacts.pluck(:id)
+ build_ids = build_ids.map { |build_id| [build_id] }
+
+ Sidekiq::Client.push_bulk('class' => ExpireBuildInstanceArtifactsWorker, 'args' => build_ids )
end
end
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
new file mode 100644
index 00000000000..916c2e633c1
--- /dev/null
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -0,0 +1,11 @@
+class ExpireBuildInstanceArtifactsWorker
+ include Sidekiq::Worker
+
+ def perform(build_id)
+ build = Ci::Build.with_expired_artifacts.reorder(nil).find_by(id: build_id)
+ return unless build
+
+ Rails.logger.info "Removing artifacts build #{build.id}..."
+ build.erase_artifacts!
+ end
+end
diff --git a/app/workers/process_pipeline_worker.rb b/app/workers/process_pipeline_worker.rb
new file mode 100644
index 00000000000..26ea5f1c24d
--- /dev/null
+++ b/app/workers/process_pipeline_worker.rb
@@ -0,0 +1,10 @@
+class ProcessPipelineWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:process!)
+ end
+end
diff --git a/app/workers/trending_projects_worker.rb b/app/workers/trending_projects_worker.rb
new file mode 100644
index 00000000000..df4c4a6628b
--- /dev/null
+++ b/app/workers/trending_projects_worker.rb
@@ -0,0 +1,11 @@
+class TrendingProjectsWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :trending_projects
+
+ def perform
+ Rails.logger.info('Refreshing trending projects')
+
+ TrendingProject.refresh!
+ end
+end
diff --git a/app/workers/update_pipeline_worker.rb b/app/workers/update_pipeline_worker.rb
new file mode 100644
index 00000000000..6ef5678073e
--- /dev/null
+++ b/app/workers/update_pipeline_worker.rb
@@ -0,0 +1,10 @@
+class UpdatePipelineWorker
+ include Sidekiq::Worker
+
+ sidekiq_options queue: :default
+
+ def perform(pipeline_id)
+ Ci::Pipeline.find_by(id: pipeline_id)
+ .try(:update_status)
+ end
+end
diff --git a/config/application.rb b/config/application.rb
index 4792f6670a8..962ffe0708d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -50,6 +50,7 @@ module Gitlab
# - Build variables (:variables)
# - GitLab Pages SSL cert/key info (:certificate, :encrypted_key)
# - Webhook URLs (:hook)
+ # - GitLab-shell secret token (:secret_token)
# - Sentry DSN (:sentry_dsn)
# - Deploy keys (:key)
config.filter_parameters += %i(
@@ -62,6 +63,7 @@ module Gitlab
password
password_confirmation
private_token
+ secret_token
sentry_dsn
variables
)
@@ -99,13 +101,24 @@ module Gitlab
config.action_view.sanitized_allowed_protocols = %w(smb)
- config.middleware.use Rack::Attack
+ config.middleware.insert_before Warden::Manager, Rack::Attack
# Allow access to GitLab API from other domains
- config.middleware.use Rack::Cors do
+ config.middleware.insert_before Warden::Manager, Rack::Cors do
+ allow do
+ origins Gitlab.config.gitlab.url
+ resource '/api/*',
+ credentials: true,
+ headers: :any,
+ methods: :any,
+ expose: ['Link']
+ end
+
+ # Cross-origin requests must not have the session cookie available
allow do
origins '*'
resource '/api/*',
+ credentials: false,
headers: :any,
methods: :any,
expose: ['Link']
@@ -116,6 +129,10 @@ module Gitlab
redis_config_hash = Gitlab::Redis.params
redis_config_hash[:namespace] = Gitlab::Redis::CACHE_NAMESPACE
redis_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
+ if Sidekiq.server? # threaded context
+ redis_config_hash[:pool_size] = Sidekiq.options[:concurrency] + 5
+ redis_config_hash[:pool_timeout] = 1
+ end
config.cache_store = :redis_store, redis_config_hash
config.active_record.raise_in_transactional_callbacks = true
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index b26c9f7ccc9..114ceac8e1f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -70,6 +70,7 @@ production: &base
email_from: example@example.com
email_display_name: GitLab
email_reply_to: noreply@example.com
+ email_subject_suffix: ''
# Email server smtp settings are in config/initializers/smtp_settings.rb.sample
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 195108b921b..efe0ac9c965 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -186,6 +186,7 @@ Settings.gitlab['email_enabled'] ||= true if Settings.gitlab['email_enabled'].ni
Settings.gitlab['email_from'] ||= ENV['GITLAB_EMAIL_FROM'] || "gitlab@#{Settings.gitlab.host}"
Settings.gitlab['email_display_name'] ||= ENV['GITLAB_EMAIL_DISPLAY_NAME'] || 'GitLab'
Settings.gitlab['email_reply_to'] ||= ENV['GITLAB_EMAIL_REPLY_TO'] || "noreply@#{Settings.gitlab.host}"
+Settings.gitlab['email_subject_suffix'] ||= ENV['GITLAB_EMAIL_SUBJECT_SUFFIX'] || ""
Settings.gitlab['base_url'] ||= Settings.send(:build_base_gitlab_url)
Settings.gitlab['url'] ||= Settings.send(:build_gitlab_url)
Settings.gitlab['user'] ||= 'git'
@@ -303,6 +304,10 @@ Settings.cron_jobs['prune_old_events_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['prune_old_events_worker']['cron'] ||= '* */6 * * *'
Settings.cron_jobs['prune_old_events_worker']['job_class'] = 'PruneOldEventsWorker'
+Settings.cron_jobs['trending_projects_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['trending_projects_worker']['cron'] = '0 1 * * *'
+Settings.cron_jobs['trending_projects_worker']['job_class'] = 'TrendingProjectsWorker'
+
#
# GitLab Shell
#
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
new file mode 100644
index 00000000000..ae2ca258df1
--- /dev/null
+++ b/config/initializers/7_redis.rb
@@ -0,0 +1,3 @@
+# Make sure we initialize a Redis connection pool before Sidekiq starts
+# multi-threaded execution.
+Gitlab::Redis.with { nil }
diff --git a/config/initializers/ar5_batching.rb b/config/initializers/ar5_batching.rb
new file mode 100644
index 00000000000..35e8b3808e2
--- /dev/null
+++ b/config/initializers/ar5_batching.rb
@@ -0,0 +1,41 @@
+# Port ActiveRecord::Relation#in_batches from ActiveRecord 5.
+# https://github.com/rails/rails/blob/ac027338e4a165273607dccee49a3d38bc836794/activerecord/lib/active_record/relation/batches.rb#L184
+# TODO: this can be removed once we're using AR5.
+raise "Vendored ActiveRecord 5 code! Delete #{__FILE__}!" if ActiveRecord::VERSION::MAJOR >= 5
+
+module ActiveRecord
+ module Batches
+ # Differences from upstream: enumerator support was removed, and custom
+ # order/limit clauses are ignored without a warning.
+ def in_batches(of: 1000, start: nil, finish: nil, load: false)
+ raise "Must provide a block" unless block_given?
+
+ relation = self.reorder(batch_order).limit(of)
+ relation = relation.where(arel_table[primary_key].gteq(start)) if start
+ relation = relation.where(arel_table[primary_key].lteq(finish)) if finish
+ batch_relation = relation
+
+ loop do
+ if load
+ records = batch_relation.records
+ ids = records.map(&:id)
+ yielded_relation = self.where(primary_key => ids)
+ yielded_relation.load_records(records)
+ else
+ ids = batch_relation.pluck(primary_key)
+ yielded_relation = self.where(primary_key => ids)
+ end
+
+ break if ids.empty?
+
+ primary_key_offset = ids.last
+ raise ArgumentError.new("Primary key not included in the custom select clause") unless primary_key_offset
+
+ yield yielded_relation
+
+ break if ids.length < of
+ batch_relation = relation.where(arel_table[primary_key].gt(primary_key_offset))
+ end
+ end
+ end
+end
diff --git a/config/initializers/ar_speed_up_migration_checking.rb b/config/initializers/ar_speed_up_migration_checking.rb
new file mode 100644
index 00000000000..1fe5defc01d
--- /dev/null
+++ b/config/initializers/ar_speed_up_migration_checking.rb
@@ -0,0 +1,18 @@
+if Rails.env.test?
+ require 'active_record/migration'
+
+ module ActiveRecord
+ class Migrator
+ class << self
+ alias_method :migrations_unmemoized, :migrations
+
+ # This method is called a large number of times per rspec example, and
+ # it reads + parses `db/migrate/*` each time. Memoizing it can save 0.5
+ # seconds per spec.
+ def migrations(paths)
+ @migrations ||= migrations_unmemoized(paths)
+ end
+ end
+ end
+ end
+end
diff --git a/config/initializers/attr_encrypted_no_db_connection.rb b/config/initializers/attr_encrypted_no_db_connection.rb
index c668864089b..e007666b852 100644
--- a/config/initializers/attr_encrypted_no_db_connection.rb
+++ b/config/initializers/attr_encrypted_no_db_connection.rb
@@ -1,20 +1,21 @@
module AttrEncrypted
module Adapters
module ActiveRecord
- def attribute_instance_methods_as_symbols_with_no_db_connection
- # Use with_connection so the connection doesn't stay pinned to the thread.
- connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
-
- if connected
- # Call version from AttrEncrypted::Adapters::ActiveRecord
- attribute_instance_methods_as_symbols_without_no_db_connection
- else
- # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
- AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
+ module DBConnectionQuerier
+ def attribute_instance_methods_as_symbols
+ # Use with_connection so the connection doesn't stay pinned to the thread.
+ connected = ::ActiveRecord::Base.connection_pool.with_connection(&:active?) rescue false
+
+ if connected
+ # Call version from AttrEncrypted::Adapters::ActiveRecord
+ super
+ else
+ # Call version from AttrEncrypted, i.e., `super` with regards to AttrEncrypted::Adapters::ActiveRecord
+ AttrEncrypted.instance_method(:attribute_instance_methods_as_symbols).bind(self).call
+ end
end
end
-
- alias_method_chain :attribute_instance_methods_as_symbols, :no_db_connection
+ prepend DBConnectionQuerier
end
end
end
diff --git a/config/initializers/connection_fix.rb b/config/initializers/connection_fix.rb
index d831a1838ed..d0b1444f607 100644
--- a/config/initializers/connection_fix.rb
+++ b/config/initializers/connection_fix.rb
@@ -20,7 +20,7 @@ if defined?(ActiveRecord::ConnectionAdapters::Mysql2Adapter)
execute_without_retry(*args)
rescue ActiveRecord::StatementInvalid => e
if e.message =~ /server has gone away/i
- warn "Server timed out, retrying"
+ warn "Lost connection to MySQL server during query"
reconnect!
retry
else
diff --git a/config/initializers/gitlab_shell_secret_token.rb b/config/initializers/gitlab_shell_secret_token.rb
index 7454c33c9dd..529dcdd4644 100644
--- a/config/initializers/gitlab_shell_secret_token.rb
+++ b/config/initializers/gitlab_shell_secret_token.rb
@@ -1 +1 @@
-Gitlab::Shell.new.generate_and_link_secret_token
+Gitlab::Shell.ensure_secret_token!
diff --git a/config/initializers/postgresql_limit_fix.rb b/config/initializers/postgresql_limit_fix.rb
index 0cb3aaf4d24..4224d857e8a 100644
--- a/config/initializers/postgresql_limit_fix.rb
+++ b/config/initializers/postgresql_limit_fix.rb
@@ -1,5 +1,19 @@
if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
class ActiveRecord::ConnectionAdapters::PostgreSQLAdapter
+ module LimitFilter
+ def add_column(table_name, column_name, type, options = {})
+ options.delete(:limit) if type == :text
+ super(table_name, column_name, type, options)
+ end
+
+ def change_column(table_name, column_name, type, options = {})
+ options.delete(:limit) if type == :text
+ super(table_name, column_name, type, options)
+ end
+ end
+
+ prepend ActiveRecord::ConnectionAdapters::PostgreSQLAdapter::LimitFilter
+
class TableDefinition
def text(*args)
options = args.extract_options!
@@ -9,18 +23,5 @@ if defined?(ActiveRecord::ConnectionAdapters::PostgreSQLAdapter)
column_names.each { |name| column(name, type, options) }
end
end
-
- def add_column_with_limit_filter(table_name, column_name, type, options = {})
- options.delete(:limit) if type == :text
- add_column_without_limit_filter(table_name, column_name, type, options)
- end
-
- def change_column_with_limit_filter(table_name, column_name, type, options = {})
- options.delete(:limit) if type == :text
- change_column_without_limit_filter(table_name, column_name, type, options)
- end
-
- alias_method_chain :add_column, :limit_filter
- alias_method_chain :change_column, :limit_filter
end
end
diff --git a/config/initializers/sentry.rb b/config/initializers/sentry.rb
index 5892c1de024..4f30d1265c8 100644
--- a/config/initializers/sentry.rb
+++ b/config/initializers/sentry.rb
@@ -18,6 +18,8 @@ if Rails.env.production?
# Sanitize fields based on those sanitized from Rails.
config.sanitize_fields = Rails.application.config.filter_parameters.map(&:to_s)
+ # Sanitize authentication headers
+ config.sanitize_http_headers = %w[Authorization Private-Token]
config.tags = { program: Gitlab::Sentry.program_context }
end
end
diff --git a/config/routes.rb b/config/routes.rb
index 4d6ec699cbd..83c3a42c19f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -2,34 +2,13 @@ require 'sidekiq/web'
require 'sidekiq/cron/web'
require 'api/api'
-Rails.application.routes.draw do
- if Gitlab::Sherlock.enabled?
- namespace :sherlock do
- resources :transactions, only: [:index, :show] do
- resources :queries, only: [:show]
- resources :file_samples, only: [:show]
-
- collection do
- delete :destroy_all
- end
- end
- end
- end
-
- if Rails.env.development?
- # Make the built-in Rails routes available in development, otherwise they'd
- # get swallowed by the `namespace/project` route matcher below.
- #
- # See https://git.io/va79N
- get '/rails/mailers' => 'rails/mailers#index'
- get '/rails/mailers/:path' => 'rails/mailers#preview'
- get '/rails/info/properties' => 'rails/info#properties'
- get '/rails/info/routes' => 'rails/info#routes'
- get '/rails/info' => 'rails/info#index'
-
- mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
+class ActionDispatch::Routing::Mapper
+ def draw(routes_name)
+ instance_eval(File.read(Rails.root.join("config/routes/#{routes_name}.rb")))
end
+end
+Rails.application.routes.draw do
concern :access_requestable do
post :request_access, on: :collection
post :approve_access_request, on: :member
@@ -39,21 +18,9 @@ Rails.application.routes.draw do
post :toggle_award_emoji, on: :member
end
- namespace :ci do
- # CI API
- Ci::API::API.logger Rails.logger
- mount Ci::API::API => '/api'
-
- resource :lint, only: [:show, :create]
-
- resources :projects, only: [:index, :show] do
- member do
- get :status, to: 'projects#badge'
- end
- end
-
- root to: 'projects#index'
- end
+ draw :sherlock
+ draw :development
+ draw :ci
use_doorkeeper do
controllers applications: 'oauth/applications',
@@ -76,44 +43,18 @@ Rails.application.routes.draw do
# JSON Web Token
get 'jwt/auth' => 'jwt#auth'
- # API
- API::API.logger Rails.logger
- mount API::API => '/api'
-
- constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
- constraints constraint do
- mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
- end
-
# Health check
get 'health_check(/:checks)' => 'health_check#index', as: :health_check
- # Help
- get 'help' => 'help#index'
- get 'help/shortcuts' => 'help#shortcuts'
- get 'help/ui' => 'help#ui'
- get 'help/*path' => 'help#show', as: :help_page
-
- #
# Koding route
- #
get 'koding' => 'koding#index'
- #
- # Global snippets
- #
- resources :snippets, concerns: :awardable do
- member do
- get 'raw'
- end
- end
-
- get '/s/:username', to: redirect('/u/%{username}/snippets'),
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
+ draw :api
+ draw :sidekiq
+ draw :help
+ draw :snippets
- #
# Invites
- #
resources :invites, only: [:show], constraints: { id: /[A-Za-z0-9_-]+/ } do
member do
post :accept
@@ -127,809 +68,24 @@ Rails.application.routes.draw do
end
end
- #
# Spam reports
- #
resources :abuse_reports, only: [:new, :create]
- #
# Notification settings
- #
resources :notification_settings, only: [:create, :update]
- #
- # Import
- #
- namespace :import do
- resource :github, only: [:create, :new], controller: :github do
- post :personal_access_token
- get :status
- get :callback
- get :jobs
- end
-
- resource :gitlab, only: [:create], controller: :gitlab do
- get :status
- get :callback
- get :jobs
- end
-
- resource :bitbucket, only: [:create], controller: :bitbucket do
- get :status
- get :callback
- get :jobs
- end
-
- resource :google_code, only: [:create, :new], controller: :google_code do
- get :status
- post :callback
- get :jobs
-
- get :new_user_map, path: :user_map
- post :create_user_map, path: :user_map
- end
-
- resource :fogbugz, only: [:create, :new], controller: :fogbugz do
- get :status
- post :callback
- get :jobs
-
- get :new_user_map, path: :user_map
- post :create_user_map, path: :user_map
- end
-
- resource :gitlab_project, only: [:create, :new] do
- post :create
- end
- end
-
- #
- # Uploads
- #
-
- scope path: :uploads do
- # Note attachments and User/Group/Project avatars
- get ":model/:mounted_as/:id/:filename",
- to: "uploads#show",
- constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
-
- # Appearance
- get ":model/:mounted_as/:id/:filename",
- to: "uploads#show",
- constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
-
- # Project markdown uploads
- get ":namespace_id/:project_id/:secret/:filename",
- to: "projects/uploads#show",
- constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
- end
-
- # Redirect old note attachments path to new uploads path.
- get "files/note/:id/:filename",
- to: redirect("uploads/note/attachment/%{id}/%{filename}"),
- constraints: { filename: /[^\/]+/ }
-
- #
- # Explore area
- #
- namespace :explore do
- resources :projects, only: [:index] do
- collection do
- get :trending
- get :starred
- end
- end
-
- resources :groups, only: [:index]
- resources :snippets, only: [:index]
- root to: 'projects#trending'
- end
-
- # Compatibility with old routing
- get 'public' => 'explore/projects#index'
- get 'public/projects' => 'explore/projects#index'
-
- #
- # Admin Area
- #
- namespace :admin do
- resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
- resources :keys, only: [:show, :destroy]
- resources :identities, except: [:show]
-
- member do
- get :projects
- get :keys
- get :groups
- put :block
- put :unblock
- put :unlock
- put :confirm
- post :impersonate
- patch :disable_two_factor
- delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
- end
- end
-
- resource :impersonation, only: :destroy
-
- resources :abuse_reports, only: [:index, :destroy]
- resources :spam_logs, only: [:index, :destroy] do
- member do
- post :mark_as_ham
- end
- end
-
- resources :applications
-
- resources :groups, constraints: { id: /[^\/]+/ } do
- member do
- put :members_update
- end
- end
-
- resources :deploy_keys, only: [:index, :new, :create, :destroy]
-
- resources :hooks, only: [:index, :create, :destroy] do
- get :test
- end
-
- resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
- post :preview, on: :collection
- end
-
- resource :logs, only: [:show]
- resource :health_check, controller: 'health_check', only: [:show]
- resource :background_jobs, controller: 'background_jobs', only: [:show]
- resource :system_info, controller: 'system_info', only: [:show]
- resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
-
- resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- root to: 'projects#index', as: :projects
-
- resources(:projects,
- path: '/',
- constraints: { id: /[a-zA-Z.0-9_\-]+/ },
- only: [:index, :show]) do
- root to: 'projects#show'
-
- member do
- put :transfer
- post :repository_check
- end
-
- resources :runner_projects, only: [:create, :destroy]
- end
- end
-
- resource :appearances, only: [:show, :create, :update], path: 'appearance' do
- member do
- get :preview
- delete :logo
- delete :header_logos
- end
- end
-
- resource :application_settings, only: [:show, :update] do
- resources :services, only: [:index, :edit, :update]
- put :reset_runners_token
- put :reset_health_check_token
- put :clear_repository_check_states
- end
-
- resources :labels
-
- resources :runners, only: [:index, :show, :update, :destroy] do
- member do
- get :resume
- get :pause
- end
- end
-
- resources :builds, only: :index do
- collection do
- post :cancel_all
- end
- end
-
- root to: 'dashboard#index'
- end
-
- #
- # Profile Area
- #
- resource :profile, only: [:show, :update] do
- member do
- get :audit_log
- get :applications, to: 'oauth/applications#index'
-
- put :reset_private_token
- put :update_username
- end
-
- scope module: :profiles do
- resource :account, only: [:show] do
- member do
- delete :unlink
- end
- end
- resource :notifications, only: [:show, :update]
- resource :password, only: [:new, :create, :edit, :update] do
- member do
- put :reset
- end
- end
- resource :preferences, only: [:show, :update]
- resources :keys, only: [:index, :show, :new, :create, :destroy]
- resources :emails, only: [:index, :create, :destroy]
- resource :avatar, only: [:destroy]
-
- resources :personal_access_tokens, only: [:index, :create] do
- member do
- put :revoke
- end
- end
-
- resource :two_factor_auth, only: [:show, :create, :destroy] do
- member do
- post :create_u2f
- post :codes
- patch :skip
- end
- end
-
- resources :u2f_registrations, only: [:destroy]
- end
- end
-
- scope(path: 'u/:username',
- as: :user,
- constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
- controller: :users) do
- get :calendar
- get :calendar_activities
- get :groups
- get :projects
- get :contributed, as: :contributed_projects
- get :snippets
- get '/', action: :show
- end
-
- #
- # Dashboard Area
- #
- resource :dashboard, controller: 'dashboard', only: [] do
- get :issues
- get :merge_requests
- get :activity
-
- scope module: :dashboard do
- resources :milestones, only: [:index, :show]
- resources :labels, only: [:index]
-
- resources :groups, only: [:index]
- resources :snippets, only: [:index]
-
- resources :todos, only: [:index, :destroy] do
- collection do
- delete :destroy_all
- end
- end
-
- resources :projects, only: [:index] do
- collection do
- get :starred
- end
- end
- end
-
- root to: "dashboard/projects#index"
- end
-
- #
- # Groups Area
- #
- resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
- member do
- get :issues
- get :merge_requests
- get :projects
- get :activity
- end
-
- scope module: :groups do
- resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
- post :resend_invite, on: :member
- delete :leave, on: :collection
- end
-
- resource :avatar, only: [:destroy]
- resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
- end
- end
-
- resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
-
- devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
- registrations: :registrations,
- passwords: :passwords,
- sessions: :sessions,
- confirmations: :confirmations }
-
- devise_scope :user do
- get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
- get '/users/almost_there' => 'confirmations#almost_there'
- end
-
- root to: "root#index"
-
- #
- # Project Area
- #
- resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
- resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
- [:new, :create, :index], path: "/") do
- member do
- put :transfer
- delete :remove_fork
- post :archive
- post :unarchive
- post :housekeeping
- post :toggle_star
- post :preview_markdown
- post :export
- post :remove_export
- post :generate_new_export
- get :download_export
- get :autocomplete_sources
- get :activity
- get :refs
- end
-
- scope module: :projects do
- scope constraints: { id: /.+\.git/, format: nil } do
- # Git HTTP clients ('git clone' etc.)
- get '/info/refs', to: 'git_http#info_refs'
- post '/git-upload-pack', to: 'git_http#git_upload_pack'
- post '/git-receive-pack', to: 'git_http#git_receive_pack'
-
- # Git LFS API (metadata)
- post '/info/lfs/objects/batch', to: 'lfs_api#batch'
- post '/info/lfs/objects', to: 'lfs_api#deprecated'
- get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
-
- # GitLab LFS object storage
- scope constraints: { oid: /[a-f0-9]{64}/ } do
- get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
-
- scope constraints: { size: /[0-9]+/ } do
- put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
- put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
- end
- end
- end
-
- # Allow /info/refs, /info/refs?service=git-upload-pack, and
- # /info/refs?service=git-receive-pack, but nothing else.
- #
- git_http_handshake = lambda do |request|
- request.query_string.blank? ||
- request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
- end
-
- ref_redirect = redirect do |params, request|
- path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
- path << "?#{request.query_string}" unless request.query_string.blank?
- path
- end
-
- get '/info/refs', constraints: git_http_handshake, to: ref_redirect
-
- # Blob routes:
- get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
- post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
- get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
- put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
- post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
-
- #
- # Templates
- #
- get '/templates/:template_type/:key' => 'templates#show', as: :template
-
- scope do
- get(
- '/blob/*id/diff',
- to: 'blob#diff',
- constraints: { id: /.+/, format: false },
- as: :blob_diff
- )
- get(
- '/blob/*id',
- to: 'blob#show',
- constraints: { id: /.+/, format: false },
- as: :blob
- )
- delete(
- '/blob/*id',
- to: 'blob#destroy',
- constraints: { id: /.+/, format: false }
- )
- put(
- '/blob/*id',
- to: 'blob#update',
- constraints: { id: /.+/, format: false }
- )
- post(
- '/blob/*id',
- to: 'blob#create',
- constraints: { id: /.+/, format: false }
- )
- end
-
- scope do
- get(
- '/raw/*id',
- to: 'raw#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :raw
- )
- end
-
- scope do
- get(
- '/tree/*id',
- to: 'tree#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :tree
- )
- end
-
- scope do
- get(
- '/find_file/*id',
- to: 'find_file#show',
- constraints: { id: /.+/, format: /html/ },
- as: :find_file
- )
- end
-
- scope do
- get(
- '/files/*id',
- to: 'find_file#list',
- constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
- as: :files
- )
- end
-
- scope do
- post(
- '/create_dir/*id',
- to: 'tree#create_dir',
- constraints: { id: /.+/ },
- as: 'create_dir'
- )
- end
-
- scope do
- get(
- '/blame/*id',
- to: 'blame#show',
- constraints: { id: /.+/, format: /(html|js)/ },
- as: :blame
- )
- end
-
- scope do
- get(
- '/commits/*id',
- to: 'commits#show',
- constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
- as: :commits
- )
- end
-
- resource :avatar, only: [:show, :destroy]
- resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
- member do
- get :branches
- get :builds
- post :cancel_builds
- post :retry_builds
- post :revert
- post :cherry_pick
- get :diff_for_path
- end
- end
-
- resources :compare, only: [:index, :create] do
- collection do
- get :diff_for_path
- end
- end
-
- get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
-
- # Don't use format parameter as file extension (old 3.0.x behavior)
- # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
- scope format: false do
- resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
-
- resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
- member do
- get :commits
- get :ci
- get :languages
- end
- end
- end
-
- resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- get 'raw'
- end
- end
-
- WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
-
- scope do
- # Order matters to give priority to these matches
- get '/wikis/git_access', to: 'wikis#git_access'
- get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
- post '/wikis', to: 'wikis#create'
-
- get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
- get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
-
- get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
- delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
- put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
- post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
- end
-
- resource :repository, only: [:create] do
- member do
- get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
- end
- end
-
- resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
- member do
- get :test
- end
- end
-
- resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
- member do
- put :enable
- put :disable
- end
- end
-
- resources :forks, only: [:index, :new, :create]
- resource :import, only: [:new, :create, :show]
-
- resources :refs, only: [] do
- collection do
- get 'switch'
- end
-
- member do
- # tree viewer logs
- get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
- # Directories with leading dots erroneously get rejected if git
- # ref regex used in constraints. Regex verification now done in controller.
- get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
- id: /.*/,
- path: /.*/
- }
- end
- end
-
- resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- get :commits
- get :diffs
- get :conflicts
- get :builds
- get :pipelines
- get :merge_check
- post :merge
- post :cancel_merge_when_build_succeeds
- get :ci_status
- post :toggle_subscription
- post :remove_wip
- get :diff_for_path
- post :resolve_conflicts
- end
-
- collection do
- get :branch_from
- get :branch_to
- get :update_branches
- get :diff_for_path
- post :bulk_update
- end
-
- resources :discussions, only: [], constraints: { id: /\h{40}/ } do
- member do
- post :resolve
- delete :resolve, action: :unresolve
- end
- end
- end
-
- resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
- resource :release, only: [:edit, :update]
- end
-
- resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
- resources :variables, only: [:index, :show, :update, :create, :destroy]
- resources :triggers, only: [:index, :create, :destroy]
-
- resources :pipelines, only: [:index, :new, :create, :show] do
- collection do
- resource :pipelines_settings, path: 'settings', only: [:show, :update]
- end
-
- member do
- post :cancel
- post :retry
- end
- end
-
- resources :environments
-
- resource :cycle_analytics, only: [:show]
-
- resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
- collection do
- post :cancel_all
-
- resources :artifacts, only: [] do
- collection do
- get :latest_succeeded,
- path: '*ref_name_and_path',
- format: false
- end
- end
- end
-
- member do
- get :status
- post :cancel
- post :retry
- post :play
- post :erase
- get :trace
- get :raw
- end
-
- resource :artifacts, only: [] do
- get :download
- get :browse, path: 'browse(/*path)', format: false
- get :file, path: 'file/*path', format: false
- post :keep
- end
- end
-
- resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
- member do
- get :test
- end
- end
-
- resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
-
- resources :milestones, constraints: { id: /\d+/ } do
- member do
- put :sort_issues
- put :sort_merge_requests
- end
- end
-
- resources :labels, except: [:show], constraints: { id: /\d+/ } do
- collection do
- post :generate
- post :set_priorities
- end
-
- member do
- post :toggle_subscription
- delete :remove_priority
- end
- end
-
- resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- post :toggle_subscription
- post :mark_as_spam
- get :referenced_merge_requests
- get :related_branches
- get :can_create_branch
- end
- collection do
- post :bulk_update
- end
- end
-
- resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
- collection do
- delete :leave
-
- # Used for import team
- # from another project
- get :import
- post :apply_import
- end
-
- member do
- post :resend_invite
- end
- end
-
- resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
-
- resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
- member do
- delete :delete_attachment
- post :resolve
- delete :resolve, action: :unresolve
- end
- end
-
- resource :board, only: [:show] do
- scope module: :boards do
- resources :issues, only: [:update]
-
- resources :lists, only: [:index, :create, :update, :destroy] do
- collection do
- post :generate
- end
-
- resources :issues, only: [:index]
- end
- end
- end
-
- resources :todos, only: [:create]
-
- resources :uploads, only: [:create] do
- collection do
- get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
- end
- end
-
- resources :runners, only: [:index, :edit, :update, :destroy, :show] do
- member do
- get :resume
- get :pause
- end
-
- collection do
- post :toggle_shared_runners
- end
- end
-
- resources :runner_projects, only: [:create, :destroy]
- resources :badges, only: [:index] do
- collection do
- scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
- constraints format: /svg/ do
- get :build
- get :coverage
- end
- end
- end
- end
- end
- end
- end
+ draw :import
+ draw :uploads
+ draw :explore
+ draw :admin
+ draw :profile
+ draw :dashboard
+ draw :group
+ draw :user
+ draw :project
# Get all keys of user
get ':username.keys' => 'profiles/keys#get_keys', constraints: { username: /.*/ }
- get ':id' => 'namespaces#show', constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ }
+ root to: "root#index"
end
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
new file mode 100644
index 00000000000..5ae985da561
--- /dev/null
+++ b/config/routes/admin.rb
@@ -0,0 +1,102 @@
+namespace :admin do
+ resources :users, constraints: { id: /[a-zA-Z.\/0-9_\-]+/ } do
+ resources :keys, only: [:show, :destroy]
+ resources :identities, except: [:show]
+
+ member do
+ get :projects
+ get :keys
+ get :groups
+ put :block
+ put :unblock
+ put :unlock
+ put :confirm
+ post :impersonate
+ patch :disable_two_factor
+ delete 'remove/:email_id', action: 'remove_email', as: 'remove_email'
+ end
+ end
+
+ resource :impersonation, only: :destroy
+
+ resources :abuse_reports, only: [:index, :destroy]
+ resources :spam_logs, only: [:index, :destroy] do
+ member do
+ post :mark_as_ham
+ end
+ end
+
+ resources :applications
+
+ resources :groups, constraints: { id: /[^\/]+/ } do
+ member do
+ put :members_update
+ end
+ end
+
+ resources :deploy_keys, only: [:index, :new, :create, :destroy]
+
+ resources :hooks, only: [:index, :create, :destroy] do
+ get :test
+ end
+
+ resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
+ post :preview, on: :collection
+ end
+
+ resource :logs, only: [:show]
+ resource :health_check, controller: 'health_check', only: [:show]
+ resource :background_jobs, controller: 'background_jobs', only: [:show]
+ resource :system_info, controller: 'system_info', only: [:show]
+ resources :requests_profiles, only: [:index, :show], param: :name, constraints: { name: /.+\.html/ }
+
+ resources :namespaces, path: '/projects', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
+ root to: 'projects#index', as: :projects
+
+ resources(:projects,
+ path: '/',
+ constraints: { id: /[a-zA-Z.0-9_\-]+/ },
+ only: [:index, :show]) do
+ root to: 'projects#show'
+
+ member do
+ put :transfer
+ post :repository_check
+ end
+
+ resources :runner_projects, only: [:create, :destroy]
+ end
+ end
+
+ resource :appearances, only: [:show, :create, :update], path: 'appearance' do
+ member do
+ get :preview
+ delete :logo
+ delete :header_logos
+ end
+ end
+
+ resource :application_settings, only: [:show, :update] do
+ resources :services, only: [:index, :edit, :update]
+ put :reset_runners_token
+ put :reset_health_check_token
+ put :clear_repository_check_states
+ end
+
+ resources :labels
+
+ resources :runners, only: [:index, :show, :update, :destroy] do
+ member do
+ get :resume
+ get :pause
+ end
+ end
+
+ resources :builds, only: :index do
+ collection do
+ post :cancel_all
+ end
+ end
+
+ root to: 'dashboard#index'
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
new file mode 100644
index 00000000000..69c8efc151c
--- /dev/null
+++ b/config/routes/api.rb
@@ -0,0 +1,2 @@
+API::API.logger Rails.logger
+mount API::API => '/api'
diff --git a/config/routes/ci.rb b/config/routes/ci.rb
new file mode 100644
index 00000000000..47a049d5b20
--- /dev/null
+++ b/config/routes/ci.rb
@@ -0,0 +1,15 @@
+namespace :ci do
+ # CI API
+ Ci::API::API.logger Rails.logger
+ mount Ci::API::API => '/api'
+
+ resource :lint, only: [:show, :create]
+
+ resources :projects, only: [:index, :show] do
+ member do
+ get :status, to: 'projects#badge'
+ end
+ end
+
+ root to: 'projects#index'
+end
diff --git a/config/routes/dashboard.rb b/config/routes/dashboard.rb
new file mode 100644
index 00000000000..fb20c63bc63
--- /dev/null
+++ b/config/routes/dashboard.rb
@@ -0,0 +1,27 @@
+resource :dashboard, controller: 'dashboard', only: [] do
+ get :issues
+ get :merge_requests
+ get :activity
+
+ scope module: :dashboard do
+ resources :milestones, only: [:index, :show]
+ resources :labels, only: [:index]
+
+ resources :groups, only: [:index]
+ resources :snippets, only: [:index]
+
+ resources :todos, only: [:index, :destroy] do
+ collection do
+ delete :destroy_all
+ end
+ end
+
+ resources :projects, only: [:index] do
+ collection do
+ get :starred
+ end
+ end
+ end
+
+ root to: "dashboard/projects#index"
+end
diff --git a/config/routes/development.rb b/config/routes/development.rb
new file mode 100644
index 00000000000..9b2b47c6a21
--- /dev/null
+++ b/config/routes/development.rb
@@ -0,0 +1,13 @@
+if Rails.env.development?
+ # Make the built-in Rails routes available in development, otherwise they'd
+ # get swallowed by the `namespace/project` route matcher below.
+ #
+ # See https://git.io/va79N
+ get '/rails/mailers' => 'rails/mailers#index'
+ get '/rails/mailers/:path' => 'rails/mailers#preview'
+ get '/rails/info/properties' => 'rails/info#properties'
+ get '/rails/info/routes' => 'rails/info#routes'
+ get '/rails/info' => 'rails/info#index'
+
+ mount LetterOpenerWeb::Engine, at: '/rails/letter_opener'
+end
diff --git a/config/routes/explore.rb b/config/routes/explore.rb
new file mode 100644
index 00000000000..42ec5e8abec
--- /dev/null
+++ b/config/routes/explore.rb
@@ -0,0 +1,16 @@
+namespace :explore do
+ resources :projects, only: [:index] do
+ collection do
+ get :trending
+ get :starred
+ end
+ end
+
+ resources :groups, only: [:index]
+ resources :snippets, only: [:index]
+ root to: 'projects#trending'
+end
+
+# Compatibility with old routing
+get 'public' => 'explore/projects#index'
+get 'public/projects' => 'explore/projects#index'
diff --git a/config/routes/group.rb b/config/routes/group.rb
new file mode 100644
index 00000000000..47a8a0a53d4
--- /dev/null
+++ b/config/routes/group.rb
@@ -0,0 +1,26 @@
+require 'constraints/group_url_constrainer'
+
+constraints(GroupUrlConstrainer.new) do
+ scope(path: ':id', as: :group, controller: :groups) do
+ get '/', action: :show
+ end
+end
+
+resources :groups, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ } do
+ member do
+ get :issues
+ get :merge_requests
+ get :projects
+ get :activity
+ end
+
+ scope module: :groups do
+ resources :group_members, only: [:index, :create, :update, :destroy], concerns: :access_requestable do
+ post :resend_invite, on: :member
+ delete :leave, on: :collection
+ end
+
+ resource :avatar, only: [:destroy]
+ resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
+ end
+end
diff --git a/config/routes/help.rb b/config/routes/help.rb
new file mode 100644
index 00000000000..d53822da9ec
--- /dev/null
+++ b/config/routes/help.rb
@@ -0,0 +1,4 @@
+get 'help' => 'help#index'
+get 'help/shortcuts' => 'help#shortcuts'
+get 'help/ui' => 'help#ui'
+get 'help/*path' => 'help#show', as: :help_page
diff --git a/config/routes/import.rb b/config/routes/import.rb
new file mode 100644
index 00000000000..89f3b3f6378
--- /dev/null
+++ b/config/routes/import.rb
@@ -0,0 +1,42 @@
+namespace :import do
+ resource :github, only: [:create, :new], controller: :github do
+ post :personal_access_token
+ get :status
+ get :callback
+ get :jobs
+ end
+
+ resource :gitlab, only: [:create], controller: :gitlab do
+ get :status
+ get :callback
+ get :jobs
+ end
+
+ resource :bitbucket, only: [:create], controller: :bitbucket do
+ get :status
+ get :callback
+ get :jobs
+ end
+
+ resource :google_code, only: [:create, :new], controller: :google_code do
+ get :status
+ post :callback
+ get :jobs
+
+ get :new_user_map, path: :user_map
+ post :create_user_map, path: :user_map
+ end
+
+ resource :fogbugz, only: [:create, :new], controller: :fogbugz do
+ get :status
+ post :callback
+ get :jobs
+
+ get :new_user_map, path: :user_map
+ post :create_user_map, path: :user_map
+ end
+
+ resource :gitlab_project, only: [:create, :new] do
+ post :create
+ end
+end
diff --git a/config/routes/profile.rb b/config/routes/profile.rb
new file mode 100644
index 00000000000..4cb68c9b34a
--- /dev/null
+++ b/config/routes/profile.rb
@@ -0,0 +1,43 @@
+resource :profile, only: [:show, :update] do
+ member do
+ get :audit_log
+ get :applications, to: 'oauth/applications#index'
+
+ put :reset_private_token
+ put :update_username
+ end
+
+ scope module: :profiles do
+ resource :account, only: [:show] do
+ member do
+ delete :unlink
+ end
+ end
+ resource :notifications, only: [:show, :update]
+ resource :password, only: [:new, :create, :edit, :update] do
+ member do
+ put :reset
+ end
+ end
+ resource :preferences, only: [:show, :update]
+ resources :keys, only: [:index, :show, :new, :create, :destroy]
+ resources :emails, only: [:index, :create, :destroy]
+ resource :avatar, only: [:destroy]
+
+ resources :personal_access_tokens, only: [:index, :create] do
+ member do
+ put :revoke
+ end
+ end
+
+ resource :two_factor_auth, only: [:show, :create, :destroy] do
+ member do
+ post :create_u2f
+ post :codes
+ patch :skip
+ end
+ end
+
+ resources :u2f_registrations, only: [:destroy]
+ end
+end
diff --git a/config/routes/project.rb b/config/routes/project.rb
new file mode 100644
index 00000000000..e8807ef06a7
--- /dev/null
+++ b/config/routes/project.rb
@@ -0,0 +1,465 @@
+resources :projects, constraints: { id: /[^\/]+/ }, only: [:index, :new, :create]
+
+resources :namespaces, path: '/', constraints: { id: /[a-zA-Z.0-9_\-]+/ }, only: [] do
+ resources(:projects, constraints: { id: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }, except:
+ [:new, :create, :index], path: "/") do
+ member do
+ put :transfer
+ delete :remove_fork
+ post :archive
+ post :unarchive
+ post :housekeeping
+ post :toggle_star
+ post :preview_markdown
+ post :export
+ post :remove_export
+ post :generate_new_export
+ get :download_export
+ get :autocomplete_sources
+ get :activity
+ get :refs
+ end
+
+ scope module: :projects do
+ scope constraints: { id: /.+\.git/, format: nil } do
+ # Git HTTP clients ('git clone' etc.)
+ get '/info/refs', to: 'git_http#info_refs'
+ post '/git-upload-pack', to: 'git_http#git_upload_pack'
+ post '/git-receive-pack', to: 'git_http#git_receive_pack'
+
+ # Git LFS API (metadata)
+ post '/info/lfs/objects/batch', to: 'lfs_api#batch'
+ post '/info/lfs/objects', to: 'lfs_api#deprecated'
+ get '/info/lfs/objects/*oid', to: 'lfs_api#deprecated'
+
+ # GitLab LFS object storage
+ scope constraints: { oid: /[a-f0-9]{64}/ } do
+ get '/gitlab-lfs/objects/*oid', to: 'lfs_storage#download'
+
+ scope constraints: { size: /[0-9]+/ } do
+ put '/gitlab-lfs/objects/*oid/*size/authorize', to: 'lfs_storage#upload_authorize'
+ put '/gitlab-lfs/objects/*oid/*size', to: 'lfs_storage#upload_finalize'
+ end
+ end
+ end
+
+ # Allow /info/refs, /info/refs?service=git-upload-pack, and
+ # /info/refs?service=git-receive-pack, but nothing else.
+ #
+ git_http_handshake = lambda do |request|
+ request.query_string.blank? ||
+ request.query_string.match(/\Aservice=git-(upload|receive)-pack\z/)
+ end
+
+ ref_redirect = redirect do |params, request|
+ path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs"
+ path << "?#{request.query_string}" unless request.query_string.blank?
+ path
+ end
+
+ get '/info/refs', constraints: git_http_handshake, to: ref_redirect
+
+ # Blob routes:
+ get '/new/*id', to: 'blob#new', constraints: { id: /.+/ }, as: 'new_blob'
+ post '/create/*id', to: 'blob#create', constraints: { id: /.+/ }, as: 'create_blob'
+ get '/edit/*id', to: 'blob#edit', constraints: { id: /.+/ }, as: 'edit_blob'
+ put '/update/*id', to: 'blob#update', constraints: { id: /.+/ }, as: 'update_blob'
+ post '/preview/*id', to: 'blob#preview', constraints: { id: /.+/ }, as: 'preview_blob'
+
+ #
+ # Templates
+ #
+ get '/templates/:template_type/:key' => 'templates#show', as: :template
+
+ scope do
+ get(
+ '/blob/*id/diff',
+ to: 'blob#diff',
+ constraints: { id: /.+/, format: false },
+ as: :blob_diff
+ )
+ get(
+ '/blob/*id',
+ to: 'blob#show',
+ constraints: { id: /.+/, format: false },
+ as: :blob
+ )
+ delete(
+ '/blob/*id',
+ to: 'blob#destroy',
+ constraints: { id: /.+/, format: false }
+ )
+ put(
+ '/blob/*id',
+ to: 'blob#update',
+ constraints: { id: /.+/, format: false }
+ )
+ post(
+ '/blob/*id',
+ to: 'blob#create',
+ constraints: { id: /.+/, format: false }
+ )
+ end
+
+ scope do
+ get(
+ '/raw/*id',
+ to: 'raw#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :raw
+ )
+ end
+
+ scope do
+ get(
+ '/tree/*id',
+ to: 'tree#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :tree
+ )
+ end
+
+ scope do
+ get(
+ '/find_file/*id',
+ to: 'find_file#show',
+ constraints: { id: /.+/, format: /html/ },
+ as: :find_file
+ )
+ end
+
+ scope do
+ get(
+ '/files/*id',
+ to: 'find_file#list',
+ constraints: { id: /(?:[^.]|\.(?!json$))+/, format: /json/ },
+ as: :files
+ )
+ end
+
+ scope do
+ post(
+ '/create_dir/*id',
+ to: 'tree#create_dir',
+ constraints: { id: /.+/ },
+ as: 'create_dir'
+ )
+ end
+
+ scope do
+ get(
+ '/blame/*id',
+ to: 'blame#show',
+ constraints: { id: /.+/, format: /(html|js)/ },
+ as: :blame
+ )
+ end
+
+ scope do
+ get(
+ '/commits/*id',
+ to: 'commits#show',
+ constraints: { id: /(?:[^.]|\.(?!atom$))+/, format: /atom/ },
+ as: :commits
+ )
+ end
+
+ resource :avatar, only: [:show, :destroy]
+ resources :commit, only: [:show], constraints: { id: /\h{7,40}/ } do
+ member do
+ get :branches
+ get :builds
+ get :pipelines
+ post :cancel_builds
+ post :retry_builds
+ post :revert
+ post :cherry_pick
+ get :diff_for_path
+ end
+ end
+
+ resources :compare, only: [:index, :create] do
+ collection do
+ get :diff_for_path
+ end
+ end
+
+ get '/compare/:from...:to', to: 'compare#show', as: 'compare', constraints: { from: /.+/, to: /.+/ }
+
+ # Don't use format parameter as file extension (old 3.0.x behavior)
+ # See http://guides.rubyonrails.org/routing.html#route-globbing-and-wildcard-segments
+ scope format: false do
+ resources :network, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex }
+
+ resources :graphs, only: [:show], constraints: { id: Gitlab::Regex.git_reference_regex } do
+ member do
+ get :commits
+ get :ci
+ get :languages
+ end
+ end
+ end
+
+ resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ get 'raw'
+ end
+ end
+
+ WIKI_SLUG_ID = { id: /\S+/ } unless defined? WIKI_SLUG_ID
+
+ scope do
+ # Order matters to give priority to these matches
+ get '/wikis/git_access', to: 'wikis#git_access'
+ get '/wikis/pages', to: 'wikis#pages', as: 'wiki_pages'
+ post '/wikis', to: 'wikis#create'
+
+ get '/wikis/*id/history', to: 'wikis#history', as: 'wiki_history', constraints: WIKI_SLUG_ID
+ get '/wikis/*id/edit', to: 'wikis#edit', as: 'wiki_edit', constraints: WIKI_SLUG_ID
+
+ get '/wikis/*id', to: 'wikis#show', as: 'wiki', constraints: WIKI_SLUG_ID
+ delete '/wikis/*id', to: 'wikis#destroy', constraints: WIKI_SLUG_ID
+ put '/wikis/*id', to: 'wikis#update', constraints: WIKI_SLUG_ID
+ post '/wikis/*id/preview_markdown', to: 'wikis#preview_markdown', constraints: WIKI_SLUG_ID, as: 'wiki_preview_markdown'
+ end
+
+ resource :repository, only: [:create] do
+ member do
+ get 'archive', constraints: { format: Gitlab::Regex.archive_formats_regex }
+ end
+ end
+
+ resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
+ member do
+ get :test
+ end
+ end
+
+ resources :deploy_keys, constraints: { id: /\d+/ }, only: [:index, :new, :create] do
+ member do
+ put :enable
+ put :disable
+ end
+ end
+
+ resources :forks, only: [:index, :new, :create]
+ resource :import, only: [:new, :create, :show]
+
+ resources :refs, only: [] do
+ collection do
+ get 'switch'
+ end
+
+ member do
+ # tree viewer logs
+ get 'logs_tree', constraints: { id: Gitlab::Regex.git_reference_regex }
+ # Directories with leading dots erroneously get rejected if git
+ # ref regex used in constraints. Regex verification now done in controller.
+ get 'logs_tree/*path' => 'refs#logs_tree', as: :logs_file, constraints: {
+ id: /.*/,
+ path: /.*/
+ }
+ end
+ end
+
+ resources :merge_requests, concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ get :commits
+ get :diffs
+ get :conflicts
+ get :builds
+ get :pipelines
+ get :merge_check
+ post :merge
+ post :cancel_merge_when_build_succeeds
+ get :ci_status
+ post :toggle_subscription
+ post :remove_wip
+ get :diff_for_path
+ post :resolve_conflicts
+ end
+
+ collection do
+ get :branch_from
+ get :branch_to
+ get :update_branches
+ get :diff_for_path
+ post :bulk_update
+ get :new_diffs, path: 'new/diffs'
+ end
+
+ resources :discussions, only: [], constraints: { id: /\h{40}/ } do
+ member do
+ post :resolve
+ delete :resolve, action: :unresolve
+ end
+ end
+ end
+
+ resources :branches, only: [:index, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :tags, only: [:index, :show, :new, :create, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex } do
+ resource :release, only: [:edit, :update]
+ end
+
+ resources :protected_branches, only: [:index, :show, :create, :update, :destroy], constraints: { id: Gitlab::Regex.git_reference_regex }
+ resources :variables, only: [:index, :show, :update, :create, :destroy]
+ resources :triggers, only: [:index, :create, :destroy]
+
+ resources :pipelines, only: [:index, :new, :create, :show] do
+ collection do
+ resource :pipelines_settings, path: 'settings', only: [:show, :update]
+ end
+
+ member do
+ post :cancel
+ post :retry
+ end
+ end
+
+ resources :environments
+
+ resource :cycle_analytics, only: [:show]
+
+ resources :builds, only: [:index, :show], constraints: { id: /\d+/ } do
+ collection do
+ post :cancel_all
+
+ resources :artifacts, only: [] do
+ collection do
+ get :latest_succeeded,
+ path: '*ref_name_and_path',
+ format: false
+ end
+ end
+ end
+
+ member do
+ get :status
+ post :cancel
+ post :retry
+ post :play
+ post :erase
+ get :trace
+ get :raw
+ end
+
+ resource :artifacts, only: [] do
+ get :download
+ get :browse, path: 'browse(/*path)', format: false
+ get :file, path: 'file/*path', format: false
+ post :keep
+ end
+ end
+
+ resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
+ member do
+ get :test
+ end
+ end
+
+ resources :container_registry, only: [:index, :destroy], constraints: { id: Gitlab::Regex.container_registry_reference_regex }
+
+ resources :milestones, constraints: { id: /\d+/ } do
+ member do
+ put :sort_issues
+ put :sort_merge_requests
+ end
+ end
+
+ resources :labels, except: [:show], constraints: { id: /\d+/ } do
+ collection do
+ post :generate
+ post :set_priorities
+ end
+
+ member do
+ post :toggle_subscription
+ delete :remove_priority
+ end
+ end
+
+ resources :issues, concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ post :toggle_subscription
+ post :mark_as_spam
+ get :referenced_merge_requests
+ get :related_branches
+ get :can_create_branch
+ end
+ collection do
+ post :bulk_update
+ end
+ end
+
+ resources :project_members, except: [:show, :new, :edit], constraints: { id: /[a-zA-Z.\/0-9_\-#%+]+/ }, concerns: :access_requestable do
+ collection do
+ delete :leave
+
+ # Used for import team
+ # from another project
+ get :import
+ post :apply_import
+ end
+
+ member do
+ post :resend_invite
+ end
+ end
+
+ resources :group_links, only: [:index, :create, :destroy], constraints: { id: /\d+/ }
+
+ resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ delete :delete_attachment
+ post :resolve
+ delete :resolve, action: :unresolve
+ end
+ end
+
+ resource :board, only: [:show] do
+ scope module: :boards do
+ resources :issues, only: [:update]
+
+ resources :lists, only: [:index, :create, :update, :destroy] do
+ collection do
+ post :generate
+ end
+
+ resources :issues, only: [:index, :create]
+ end
+ end
+ end
+
+ resources :todos, only: [:create]
+
+ resources :uploads, only: [:create] do
+ collection do
+ get ":secret/:filename", action: :show, as: :show, constraints: { filename: /[^\/]+/ }
+ end
+ end
+
+ resources :runners, only: [:index, :edit, :update, :destroy, :show] do
+ member do
+ get :resume
+ get :pause
+ end
+
+ collection do
+ post :toggle_shared_runners
+ end
+ end
+
+ resources :runner_projects, only: [:create, :destroy]
+ resources :badges, only: [:index] do
+ collection do
+ scope '*ref', constraints: { ref: Gitlab::Regex.git_reference_regex } do
+ constraints format: /svg/ do
+ get :build
+ get :coverage
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/config/routes/sherlock.rb b/config/routes/sherlock.rb
new file mode 100644
index 00000000000..c9969f91c36
--- /dev/null
+++ b/config/routes/sherlock.rb
@@ -0,0 +1,12 @@
+if Gitlab::Sherlock.enabled?
+ namespace :sherlock do
+ resources :transactions, only: [:index, :show] do
+ resources :queries, only: [:show]
+ resources :file_samples, only: [:show]
+
+ collection do
+ delete :destroy_all
+ end
+ end
+ end
+end
diff --git a/config/routes/sidekiq.rb b/config/routes/sidekiq.rb
new file mode 100644
index 00000000000..d3e6bc4c292
--- /dev/null
+++ b/config/routes/sidekiq.rb
@@ -0,0 +1,4 @@
+constraint = lambda { |request| request.env['warden'].authenticate? and request.env['warden'].user.admin? }
+constraints constraint do
+ mount Sidekiq::Web, at: '/admin/sidekiq', as: :sidekiq
+end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
new file mode 100644
index 00000000000..1949f215c66
--- /dev/null
+++ b/config/routes/snippets.rb
@@ -0,0 +1,8 @@
+resources :snippets, concerns: :awardable do
+ member do
+ get 'raw'
+ end
+end
+
+get '/s/:username', to: redirect('/u/%{username}/snippets'),
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
new file mode 100644
index 00000000000..2b22148a134
--- /dev/null
+++ b/config/routes/uploads.rb
@@ -0,0 +1,21 @@
+scope path: :uploads do
+ # Note attachments and User/Group/Project avatars
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: /[^\/]+/ }
+
+ # Appearance
+ get ":model/:mounted_as/:id/:filename",
+ to: "uploads#show",
+ constraints: { model: /appearance/, mounted_as: /logo|header_logo/, filename: /.+/ }
+
+ # Project markdown uploads
+ get ":namespace_id/:project_id/:secret/:filename",
+ to: "projects/uploads#show",
+ constraints: { namespace_id: /[a-zA-Z.0-9_\-]+/, project_id: /[a-zA-Z.0-9_\-]+/, filename: /[^\/]+/ }
+end
+
+# Redirect old note attachments path to new uploads path.
+get "files/note/:id/:filename",
+ to: redirect("uploads/note/attachment/%{id}/%{filename}"),
+ constraints: { filename: /[^\/]+/ }
diff --git a/config/routes/user.rb b/config/routes/user.rb
new file mode 100644
index 00000000000..54bbcb18f6a
--- /dev/null
+++ b/config/routes/user.rb
@@ -0,0 +1,37 @@
+require 'constraints/user_url_constrainer'
+
+get '/u/:username', to: redirect('/%{username}'),
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ }
+
+devise_for :users, controllers: { omniauth_callbacks: :omniauth_callbacks,
+ registrations: :registrations,
+ passwords: :passwords,
+ sessions: :sessions,
+ confirmations: :confirmations }
+
+devise_scope :user do
+ get '/users/auth/:provider/omniauth_error' => 'omniauth_callbacks#omniauth_error', as: :omniauth_error
+ get '/users/almost_there' => 'confirmations#almost_there'
+end
+
+constraints(UserUrlConstrainer.new) do
+ scope(path: ':username',
+ as: :user,
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :users) do
+ get '/', action: :show
+ end
+end
+
+scope(path: 'u/:username',
+ as: :user,
+ constraints: { username: /[a-zA-Z.0-9_\-]+(?<!\.atom)/ },
+ controller: :users) do
+ get :calendar
+ get :calendar_activities
+ get :groups
+ get :projects
+ get :contributed, as: :contributed_projects
+ get :snippets
+ get '/', to: redirect('/%{username}')
+end
diff --git a/db/fixtures/development/06_teams.rb b/db/fixtures/development/06_teams.rb
index 3e8cdcd67b4..9739a5ac8d5 100644
--- a/db/fixtures/development/06_teams.rb
+++ b/db/fixtures/development/06_teams.rb
@@ -1,7 +1,7 @@
Gitlab::Seeder.quiet do
Group.all.each do |group|
User.all.sample(4).each do |user|
- if group.add_users([user.id], Gitlab::Access.values.sample)
+ if group.add_user(user, Gitlab::Access.values.sample).persisted?
print '.'
else
print 'F'
diff --git a/db/fixtures/development/14_pipelines.rb b/db/fixtures/development/14_pipelines.rb
index 650b410595c..803cbca584d 100644
--- a/db/fixtures/development/14_pipelines.rb
+++ b/db/fixtures/development/14_pipelines.rb
@@ -34,7 +34,7 @@ class Gitlab::Seeder::Pipelines
rescue ActiveRecord::RecordInvalid
print 'F'
ensure
- pipeline.build_updated
+ pipeline.update_status
end
end
end
diff --git a/db/migrate/20160829114652_add_markdown_cache_columns.rb b/db/migrate/20160829114652_add_markdown_cache_columns.rb
new file mode 100644
index 00000000000..8753e55e058
--- /dev/null
+++ b/db/migrate/20160829114652_add_markdown_cache_columns.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddMarkdownCacheColumns < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ COLUMNS = {
+ abuse_reports: [:message],
+ appearances: [:description],
+ application_settings: [
+ :sign_in_text,
+ :help_page_text,
+ :shared_runners_text,
+ :after_sign_up_text
+ ],
+ broadcast_messages: [:message],
+ issues: [:title, :description],
+ labels: [:description],
+ merge_requests: [:title, :description],
+ milestones: [:title, :description],
+ namespaces: [:description],
+ notes: [:note],
+ projects: [:description],
+ releases: [:description],
+ snippets: [:title, :content],
+ }
+
+ def change
+ COLUMNS.each do |table, columns|
+ columns.each do |column|
+ add_column table, "#{column}_html", :text
+ end
+ end
+ end
+end
diff --git a/db/migrate/20160926145521_add_organization_to_user.rb b/db/migrate/20160926145521_add_organization_to_user.rb
new file mode 100644
index 00000000000..e0bef6e7548
--- /dev/null
+++ b/db/migrate/20160926145521_add_organization_to_user.rb
@@ -0,0 +1,12 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddOrganizationToUser < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def change
+ add_column :users, :organization, :string
+ end
+end
diff --git a/db/migrate/20161007133303_precalculate_trending_projects.rb b/db/migrate/20161007133303_precalculate_trending_projects.rb
new file mode 100644
index 00000000000..b324cd94268
--- /dev/null
+++ b/db/migrate/20161007133303_precalculate_trending_projects.rb
@@ -0,0 +1,38 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class PrecalculateTrendingProjects < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :trending_projects do |t|
+ t.references :project, index: true, foreign_key: { on_delete: :cascade }, null: false
+ end
+
+ timestamp = connection.quote(1.month.ago)
+
+ # We're hardcoding the visibility level (public) here so that if it ever
+ # changes this query doesn't suddenly use the new value (which may break
+ # later migrations).
+ visibility = 20
+
+ execute <<-EOF.strip_heredoc
+ INSERT INTO trending_projects (project_id)
+ SELECT project_id
+ FROM notes
+ INNER JOIN projects ON projects.id = notes.project_id
+ WHERE notes.created_at >= #{timestamp}
+ AND notes.system IS FALSE
+ AND projects.visibility_level = #{visibility}
+ GROUP BY project_id
+ ORDER BY count(*) DESC
+ LIMIT 100;
+ EOF
+ end
+
+ def down
+ drop_table :trending_projects
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 425fc33b7b3..c5ddf9eee32 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: 20160920160832) do
+ActiveRecord::Schema.define(version: 20161007133303) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -23,6 +23,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.text "message"
t.datetime "created_at"
t.datetime "updated_at"
+ t.text "message_html"
end
create_table "appearances", force: :cascade do |t|
@@ -30,8 +31,9 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.text "description"
t.string "header_logo"
t.string "logo"
- t.datetime "created_at", null: false
- t.datetime "updated_at", null: false
+ t.datetime "created_at", null: false
+ t.datetime "updated_at", null: false
+ t.text "description_html"
end
create_table "application_settings", force: :cascade do |t|
@@ -92,6 +94,10 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.text "domain_blacklist"
t.boolean "koding_enabled"
t.string "koding_url"
+ t.text "sign_in_text_html"
+ t.text "help_page_text_html"
+ t.text "shared_runners_text_html"
+ t.text "after_sign_up_text_html"
end
create_table "audit_events", force: :cascade do |t|
@@ -128,13 +134,14 @@ ActiveRecord::Schema.define(version: 20160920160832) do
add_index "boards", ["project_id"], name: "index_boards_on_project_id", using: :btree
create_table "broadcast_messages", force: :cascade do |t|
- t.text "message", null: false
+ t.text "message", null: false
t.datetime "starts_at"
t.datetime "ends_at"
t.datetime "created_at"
t.datetime "updated_at"
t.string "color"
t.string "font"
+ t.text "message_html"
end
create_table "ci_application_settings", force: :cascade do |t|
@@ -457,18 +464,20 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.integer "position", default: 0
+ t.integer "position", default: 0
t.string "branch_name"
t.text "description"
t.integer "milestone_id"
t.string "state"
t.integer "iid"
t.integer "updated_by_id"
- t.boolean "confidential", default: false
+ t.boolean "confidential", default: false
t.datetime "deleted_at"
t.date "due_date"
t.integer "moved_to_id"
t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "issues", ["assignee_id"], name: "index_issues_on_assignee_id", using: :btree
@@ -514,9 +523,10 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
- t.boolean "template", default: false
+ t.boolean "template", default: false
t.string "description"
t.integer "priority"
+ t.text "description_html"
end
add_index "labels", ["priority"], name: "index_labels_on_priority", using: :btree
@@ -632,6 +642,8 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.datetime "deleted_at"
t.string "in_progress_merge_commit_sha"
t.integer "lock_version"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "merge_requests", ["assignee_id"], name: "index_merge_requests_on_assignee_id", using: :btree
@@ -658,14 +670,16 @@ ActiveRecord::Schema.define(version: 20160920160832) do
add_index "merge_requests_closing_issues", ["merge_request_id"], name: "index_merge_requests_closing_issues_on_merge_request_id", using: :btree
create_table "milestones", force: :cascade do |t|
- t.string "title", null: false
- t.integer "project_id", null: false
+ t.string "title", null: false
+ t.integer "project_id", null: false
t.text "description"
t.date "due_date"
t.datetime "created_at"
t.datetime "updated_at"
t.string "state"
t.integer "iid"
+ t.text "title_html"
+ t.text "description_html"
end
add_index "milestones", ["description"], name: "index_milestones_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
@@ -689,6 +703,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.boolean "request_access_enabled", default: true, null: false
t.datetime "deleted_at"
t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "namespaces", ["created_at"], name: "index_namespaces_on_created_at", using: :btree
@@ -721,6 +736,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.integer "resolved_by_id"
t.string "discussion_id"
t.string "original_discussion_id"
+ t.text "note_html"
end
add_index "notes", ["author_id"], name: "index_notes_on_author_id", using: :btree
@@ -872,6 +888,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.boolean "request_access_enabled", default: true, null: false
t.boolean "has_external_wiki"
t.boolean "lfs_enabled"
+ t.text "description_html"
end
add_index "projects", ["ci_id"], name: "index_projects_on_ci_id", using: :btree
@@ -922,6 +939,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.integer "project_id"
t.datetime "created_at"
t.datetime "updated_at"
+ t.text "description_html"
end
add_index "releases", ["project_id", "tag"], name: "index_releases_on_project_id_and_tag", using: :btree
@@ -976,6 +994,8 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.string "file_name"
t.string "type"
t.integer "visibility_level", default: 0, null: false
+ t.text "title_html"
+ t.text "content_html"
end
add_index "snippets", ["author_id"], name: "index_snippets_on_author_id", using: :btree
@@ -1050,6 +1070,12 @@ ActiveRecord::Schema.define(version: 20160920160832) do
add_index "todos", ["target_type", "target_id"], name: "index_todos_on_target_type_and_target_id", using: :btree
add_index "todos", ["user_id"], name: "index_todos_on_user_id", using: :btree
+ create_table "trending_projects", force: :cascade do |t|
+ t.integer "project_id", null: false
+ end
+
+ add_index "trending_projects", ["project_id"], name: "index_trending_projects_on_project_id", using: :btree
+
create_table "u2f_registrations", force: :cascade do |t|
t.text "certificate"
t.string "key_handle"
@@ -1132,6 +1158,7 @@ ActiveRecord::Schema.define(version: 20160920160832) do
t.datetime "otp_grace_period_started_at"
t.boolean "ldap_email", default: false, null: false
t.boolean "external", default: false
+ t.string "organization"
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1191,5 +1218,6 @@ ActiveRecord::Schema.define(version: 20160920160832) do
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "protected_branch_merge_access_levels", "protected_branches"
add_foreign_key "protected_branch_push_access_levels", "protected_branches"
+ add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
end
diff --git a/doc/README.md b/doc/README.md
index cb378e68c60..e7712eed5d3 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -6,8 +6,8 @@
- [API](api/README.md) Automate GitLab via a simple and powerful API.
- [CI/CD](ci/README.md) GitLab Continuous Integration (CI) and Continuous Delivery (CD) getting started, `.gitlab-ci.yml` options, and examples.
- [GitLab as OAuth2 authentication service provider](integration/oauth_provider.md). It allows you to login to other applications from GitLab.
-- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
-- [GitLab Basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
+- [Container Registry](user/project/container_registry.md) Learn how to use GitLab Container Registry.
+- [GitLab basics](gitlab-basics/README.md) Find step by step how to start working on your commandline and on GitLab.
- [Importing to GitLab](workflow/importing/README.md).
- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](user/markdown.md) GitLab's advanced formatting system.
@@ -19,6 +19,7 @@
- [SSH](ssh/README.md) Setup your ssh keys and deploy keys for secure access to your projects.
- [Webhooks](web_hooks/web_hooks.md) Let GitLab notify you when new code has been pushed to your project.
- [Workflow](workflow/README.md) Using GitLab functionality and importing projects from GitHub and SVN.
+- [University](university/README.md) Learn Git and GitLab through videos and courses.
## Administrator documentation
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 7186f707ad6..bf7814875bf 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -275,3 +275,9 @@ If you are getting 'Connection Refused' errors when trying to connect to the
LDAP server please double-check the LDAP `port` and `method` settings used by
GitLab. Common combinations are `method: 'plain'` and `port: 389`, OR
`method: 'ssl'` and `port: 636`.
+
+### Login with valid credentials rejected
+
+If there is an unexpected error while authenticating the user with the LDAP
+backend, the login is rejected and details about the error are logged to
+`production.log`.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index c5611e2a121..d7cfb464f74 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -1,42 +1,32 @@
-# GitLab Container Registry Administration
+# GitLab Container Registry administration
> [Introduced][ce-4040] in GitLab 8.8.
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
---
-<!-- START doctoc generated TOC please keep comment here to allow auto update -->
-<!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE -->
-**Table of Contents** *generated with [DocToc](https://github.com/thlorenz/doctoc)*
+> **Notes:**
+- Container Registry manifest `v1` support was added in GitLab 8.9 to support
+ Docker versions earlier than 1.10.
+- This document is about the admin guide. To learn how to use GitLab Container
+ Registry [user documentation](../user/project/container_registry.md).
-- [Enable the Container Registry](#enable-the-container-registry)
-- [Container Registry domain configuration](#container-registry-domain-configuration)
- - [Configure Container Registry under an existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain)
- - [Configure Container Registry under its own domain](#configure-container-registry-under-its-own-domain)
-- [Disable Container Registry site-wide](#disable-container-registry-site-wide)
-- [Disable Container Registry per project](#disable-container-registry-per-project)
-- [Disable Container Registry for new projects site-wide](#disable-container-registry-for-new-projects-site-wide)
-- [Container Registry storage path](#container-registry-storage-path)
-- [Container Registry storage driver](#container-registry-storage-driver)
-- [Storage limitations](#storage-limitations)
-- [Changelog](#changelog)
+With the Container Registry integrated into GitLab, every project can have its
+own space to store its Docker images.
-<!-- END doctoc generated TOC please keep comment here to allow auto update -->
+You can read more about the Container Registry at
+https://docs.docker.com/registry/introduction/.
## Enable the Container Registry
**Omnibus GitLab installations**
All you have to do is configure the domain name under which the Container
-Registry will listen to. Read [#container-registry-domain-configuration](#container-registry-domain-configuration)
+Registry will listen to. Read
+[#container-registry-domain-configuration](#container-registry-domain-configuration)
and pick one of the two options that fits your case.
>**Note:**
-The container Registry works under HTTPS by default. Using HTTP is possible
+The container registry works under HTTPS by default. Using HTTP is possible
but not recommended and out of the scope of this document.
Read the [insecure Registry documentation][docker-insecure] if you want to
implement this.
@@ -47,7 +37,7 @@ implement this.
If you have installed GitLab from source:
-1. You will have to [install Docker Registry][registry-deploy] by yourself.
+1. You will have to [install Registry][registry-deploy] by yourself.
1. After the installation is complete, you will have to configure the Registry's
settings in `gitlab.yml` in order to enable it.
1. Use the sample NGINX configuration file that is found under
@@ -80,11 +70,13 @@ where:
| `issuer` | This should be the same value as configured in Registry's `issuer`. Read the [token auth configuration documentation][token-config]. |
>**Note:**
-GitLab does not ship with a Registry init file. Hence, [restarting GitLab][restart gitlab]
-will not restart the Registry should you modify its settings. Read the upstream
-documentation on how to achieve that.
+A Registry init file is not shipped with GitLab if you install it from source.
+Hence, [restarting GitLab][restart gitlab] will not restart the Registry should
+you modify its settings. Read the upstream documentation on how to achieve that.
-The Docker Registry configuration will need `container_registry` as the service and `https://gitlab.example.com/jwt/auth` as the realm:
+At the absolute minimum, make sure your [Registry configuration][registry-auth]
+has `container_registry` as the service and `https://gitlab.example.com/jwt/auth`
+as the realm:
```
auth:
@@ -275,12 +267,6 @@ Registry application itself.
1. Save the file and [restart GitLab][] for the changes to take effect.
-## Disable Container Registry per project
-
-If Registry is enabled in your GitLab instance, but you don't need it for your
-project, you can disable it from your project's settings. Read the user guide
-on how to achieve that.
-
## Disable Container Registry for new projects site-wide
If the Container Registry is enabled, then it will be available on all new
@@ -436,6 +422,46 @@ storage:
enabled: true
```
+## Change the registry's internal port
+
+> **Note:**
+This is not to be confused with the port that GitLab itself uses to expose
+the Registry to the world.
+
+The Registry server listens on localhost at port `5000` by default,
+which is the address for which the Registry server should accept connections.
+In the examples below we set the Registry's port to `5001`.
+
+**Omnibus GitLab**
+
+1. Open `/etc/gitlab/gitlab.rb` and set `registry['registry_http_addr']`:
+
+ ```ruby
+ registry['registry_http_addr'] = "localhost:5001"
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+1. Open the configuration file of your Registry server and edit the
+ [`http:addr`][registry-http-config] value:
+
+ ```
+ http
+ addr: localhost:5001
+ ```
+
+1. Save the file and restart the Registry server.
+
+## Disable Container Registry per project
+
+If Registry is enabled in your GitLab instance, but you don't need it for your
+project, you can disable it from your project's settings. Read the user guide
+on how to achieve that.
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
@@ -455,6 +481,8 @@ configurable in future releases.
[docker-insecure]: https://docs.docker.com/registry/insecure/
[registry-deploy]: https://docs.docker.com/registry/deploying/
[storage-config]: https://docs.docker.com/registry/configuration/#storage
+[registry-http-config]: https://docs.docker.com/registry/configuration/#http
+[registry-auth]: https://docs.docker.com/registry/configuration/#auth
[token-config]: https://docs.docker.com/registry/configuration/#token
[8-8-docs]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-8-stable/doc/administration/container_registry.md
[registry-ssl]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/lib/support/nginx/registry-ssl
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 7f53915a4d7..b4a953d1ccc 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -13,15 +13,17 @@ override certain values.
Variable | Type | Description
-------- | ---- | -----------
-`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
-`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
-`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
-`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
-`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
-`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
-`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
-`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_ROOT_PASSWORD` | string | Sets the password for the `root` user on installation
+`GITLAB_HOST` | string | The full URL of the GitLab server (including `http://` or `https://`)
+`RAILS_ENV` | string | The Rails environment; can be one of `production`, `development`, `staging` or `test`
+`DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`
+`GITLAB_EMAIL_FROM` | string | The e-mail address used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_DISPLAY_NAME` | string | The name used in the "From" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_REPLY_TO` | string | The e-mail address used in the "Reply-To" field in e-mails sent by GitLab
+`GITLAB_EMAIL_SUBJECT_SUFFIX` | string | The e-mail subject suffix used in e-mails sent by GitLab
+`GITLAB_UNICORN_MEMORY_MIN` | integer | The minimum memory threshold (in bytes) for the Unicorn worker killer
+`GITLAB_UNICORN_MEMORY_MAX` | integer | The maximum memory threshold (in bytes) for the Unicorn worker killer
## Complete database variables
diff --git a/doc/administration/housekeeping.md b/doc/administration/housekeeping.md
index 34b4f1faa94..ad1fa98b63b 100644
--- a/doc/administration/housekeeping.md
+++ b/doc/administration/housekeeping.md
@@ -12,7 +12,7 @@ revisions (to reduce disk space and increase performance) and removing
unreachable objects which may have been created from prior invocations of
`git add`.
-You can find this option under your **[Project] > Settings**.
+You can find this option under your **[Project] > Edit Project**.
---
diff --git a/doc/administration/img/housekeeping_settings.png b/doc/administration/img/housekeeping_settings.png
index f72ad9a45d5..6ebc6205635 100644
--- a/doc/administration/img/housekeeping_settings.png
+++ b/doc/administration/img/housekeeping_settings.png
Binary files differ
diff --git a/doc/api/README.md b/doc/api/README.md
index 8e4f7f12b4b..9e907689c80 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -101,7 +101,7 @@ Once you have your token, pass it to the API using either the `private_token`
parameter or the `PRIVATE-TOKEN` header.
-### Session cookie
+### Session Cookie
When signing in to GitLab as an ordinary user, a `_gitlab_session` cookie is
set. The API will use this cookie for authentication if it is present, but using
@@ -355,6 +355,19 @@ follows:
}
```
+## Unknown route
+
+When you try to access an API URL that does not exist you will receive 404 Not Found.
+
+```
+HTTP/1.1 404 Not Found
+Content-Type: application/json
+{
+ "error": "404 Not Found"
+}
+```
+
+
## Clients
There are many unofficial GitLab API Clients for most of the popular
diff --git a/doc/api/boards.md b/doc/api/boards.md
new file mode 100644
index 00000000000..28681719f43
--- /dev/null
+++ b/doc/api/boards.md
@@ -0,0 +1,251 @@
+# Boards
+
+Every API call to boards must be authenticated.
+
+If a user is not a member of a project and the project is private, a `GET`
+request on that project will result to a `404` status code.
+
+## Project Board
+
+Lists Issue Boards in the given project.
+
+```
+GET /projects/:id/boards
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/:id/boards
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "lists" : [
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+ ]
+ }
+]
+```
+
+## List board lists
+
+Get a list of the board's lists.
+Does not include `backlog` and `done` lists
+
+```
+GET /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists
+```
+
+Example response:
+
+```json
+[
+ {
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+ },
+ {
+ "id" : 2,
+ "label" : {
+ "name" : "Ready",
+ "color" : "#FF0000",
+ "description" : null
+ },
+ "position" : 2
+ },
+ {
+ "id" : 3,
+ "label" : {
+ "name" : "Production",
+ "color" : "#FF5F00",
+ "description" : null
+ },
+ "position" : 3
+ }
+]
+```
+
+## Single board list
+
+Get a single board list.
+
+```
+GET /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id`| integer | yes | The ID of a board's list |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## New board list
+
+Creates a new Issue Board list.
+
+If the operation is successful, a status code of `200` and the newly-created
+list is returned. If an error occurs, an error number and a message explaining
+the reason is returned.
+
+```
+POST /projects/:id/boards/:board_id/lists
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `label_id` | integer | yes | The ID of a label |
+
+```bash
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists?label_id=5
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## Edit board list
+
+Updates an existing Issue Board list. This call is used to change list position.
+
+If the operation is successful, a code of `200` and the updated board list is
+returned. If an error occurs, an error number and a message explaining the
+reason is returned.
+
+```
+PUT /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+| `position` | integer | yes | The position of the list |
+
+```bash
+curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1?position=2
+```
+
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
+
+## Delete a board list
+
+Only for admins and project owners. Soft deletes the board list in question.
+If the operation is successful, a status code `200` is returned. In case you cannot
+destroy this board list, or it is not present, code `404` is given.
+
+```
+DELETE /projects/:id/boards/:board_id/lists/:list_id
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer | yes | The ID of a project |
+| `board_id` | integer | yes | The ID of a board |
+| `list_id` | integer | yes | The ID of a board's list |
+
+```bash
+curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/projects/5/boards/1/lists/1
+```
+Example response:
+
+```json
+{
+ "id" : 1,
+ "label" : {
+ "name" : "Testing",
+ "color" : "#F0AD4E",
+ "description" : null
+ },
+ "position" : 1
+}
+```
diff --git a/doc/api/builds.md b/doc/api/builds.md
index dce666445d0..e8a9e4743d3 100644
--- a/doc/api/builds.md
+++ b/doc/api/builds.md
@@ -40,6 +40,12 @@ Example of response
"finished_at": "2015-12-24T17:54:27.895Z",
"id": 7,
"name": "teaspoon",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -78,6 +84,12 @@ Example of response
"finished_at": "2015-12-24T17:54:24.921Z",
"id": 6,
"name": "spinach:other",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -146,6 +158,12 @@ Example of response
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 69,
"name": "rubocop",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -170,6 +188,12 @@ Example of response
"finished_at": "2015-12-24T17:54:33.913Z",
"id": 9,
"name": "brakeman",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
@@ -231,6 +255,12 @@ Example of response
"finished_at": "2015-12-24T17:54:31.198Z",
"id": 8,
"name": "rubocop",
+ "pipeline": {
+ "id": 6,
+ "ref": "master",
+ "sha": "0ff3ae198f8601a285adcf5c0fff204ee6fba5fd",
+ "status": "pending"
+ }
"ref": "master",
"runner": null,
"stage": "test",
diff --git a/doc/api/ci/runners.md b/doc/api/ci/runners.md
index ecec53fde03..16028d1f124 100644
--- a/doc/api/ci/runners.md
+++ b/doc/api/ci/runners.md
@@ -12,7 +12,9 @@ communication channel. For the consumer API see the
This API uses two types of authentication:
1. Unique Runner's token, which is the token assigned to the Runner after it
- has been registered.
+ has been registered. This token can be found on the Runner's edit page (go to
+ **Project > Runners**, select one of the Runners listed under **Runners activated for
+ this project**).
2. Using Runners' registration token.
This is a token that can be found in project's settings.
@@ -48,7 +50,7 @@ DELETE /ci/api/v1/runners/delete
| Attribute | Type | Required | Description |
| --------- | ------- | --------- | ----------- |
-| `token` | string | yes | Runner's registration token |
+| `token` | string | yes | Unique Runner's token |
Example request:
diff --git a/doc/api/commits.md b/doc/api/commits.md
index 682151d4b1d..3e20beefb8a 100644
--- a/doc/api/commits.md
+++ b/doc/api/commits.md
@@ -46,6 +46,91 @@ Example response:
]
```
+## Create a commit with multiple files and actions
+
+> [Introduced][ce-6096] in GitLab 8.13.
+
+Create a commit by posting a JSON payload
+
+```
+POST /projects/:id/repository/commits
+```
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of a project or NAMESPACE/PROJECT_NAME |
+| `branch_name` | string | yes | The name of a branch |
+| `commit_message` | string | yes | Commit message |
+| `actions[]` | array | yes | An array of action hashes to commit as a batch. See the next table for what attributes it can take. |
+| `author_email` | string | no | Specify the commit author's email address |
+| `author_name` | string | no | Specify the commit author's name |
+
+
+| `actions[]` Attribute | Type | Required | Description |
+| --------------------- | ---- | -------- | ----------- |
+| `action` | string | yes | The action to perform, `create`, `delete`, `move`, `update` |
+| `file_path` | string | yes | Full path to the file. Ex. `lib/class.rb` |
+| `previous_path` | string | no | Original full path to the file being moved. Ex. `lib/class1.rb` |
+| `content` | string | no | File content, required for all except `delete`. Optional for `move` |
+| `encoding` | string | no | `text` or `base64`. `text` is default. |
+
+```bash
+PAYLOAD=$(cat << 'JSON'
+{
+ "branch_name": "master",
+ "commit_message": "some commit message",
+ "actions": [
+ {
+ "action": "create",
+ "file_path": "foo/bar",
+ "content": "some content"
+ },
+ {
+ "action": "delete",
+ "file_path": "foo/bar2",
+ },
+ {
+ "action": "move",
+ "file_path": "foo/bar3",
+ "previous_path": "foo/bar4",
+ "content": "some content"
+ },
+ {
+ "action": "update",
+ "file_path": "foo/bar5",
+ "content": "new content"
+ }
+ ]
+}
+JSON
+)
+curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" --header "Content-Type: application/json" --data "$PAYLOAD" https://gitlab.example.com/api/v3/projects/1/repository/commits
+```
+
+Example response:
+```json
+{
+ "id": "ed899a2f4b50b4370feeea94676502b42383c746",
+ "short_id": "ed899a2f4b5",
+ "title": "some commit message",
+ "author_name": "Dmitriy Zaporozhets",
+ "author_email": "dzaporozhets@sphereconsultinginc.com",
+ "created_at": "2016-09-20T09:26:24.000-07:00",
+ "message": "some commit message",
+ "parent_ids": [
+ "ae1d9fb46aa2b07ee9836d49862ec4e2c46fbbba"
+ ],
+ "committed_date": "2016-09-20T09:26:24.000-07:00",
+ "authored_date": "2016-09-20T09:26:24.000-07:00",
+ "stats": {
+ "additions": 2,
+ "deletions": 2,
+ "total": 4
+ },
+ "status": null
+}
+```
+
## Get a single commit
Get a specific commit identified by the commit hash or name of a branch or tag.
@@ -343,3 +428,5 @@ Example response:
"finished_at" : "2016-01-19T09:05:50.365Z"
}
```
+
+[ce-6096]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/6096 "Multi-file commit"
diff --git a/doc/api/labels.md b/doc/api/labels.md
index 3653ccf304a..656232cc940 100644
--- a/doc/api/labels.md
+++ b/doc/api/labels.md
@@ -148,7 +148,7 @@ PUT /projects/:id/labels
| --------------- | ------- | --------------------------------- | ------------------------------- |
| `id` | integer | yes | The ID of the project |
| `name` | string | yes | The name of the existing label |
-| `new_name` | string | yes if `color` if not provided | The new name of the label |
+| `new_name` | string | yes if `color` is not provided | The new name of the label |
| `color` | string | yes if `new_name` is not provided | The new color of the label in 6-digit hex notation with leading `#` sign |
| `description` | string | no | The new description of the label |
diff --git a/doc/api/oauth2.md b/doc/api/oauth2.md
index 0b0fc39ec7e..5ef5e3f5744 100644
--- a/doc/api/oauth2.md
+++ b/doc/api/oauth2.md
@@ -1,10 +1,10 @@
-# GitLab as an OAuth2 client
+# GitLab as an OAuth2 provider
This document covers using the OAuth2 protocol to access GitLab.
If you want GitLab to be an OAuth authentication service provider to sign into other services please see the [Oauth2 provider documentation](../integration/oauth_provider.md).
-OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
+OAuth2 is a protocol that enables us to authenticate a user without requiring them to give their password to a third-party.
This functionality is based on [doorkeeper gem](https://github.com/doorkeeper-gem/doorkeeper)
@@ -22,7 +22,7 @@ In the following sections you will be introduced to the three steps needed for t
### 1. Registering the client
First, you should create an application (`/profile/applications`) in your user's account.
-Each application gets a unique App ID and App Secret parameters.
+Each application gets a unique App ID and App Secret parameters.
>**Note:**
**You should not share/leak your App ID or App Secret.**
@@ -46,10 +46,10 @@ http://myapp.com/oauth/redirect?code=1234567890&state=your_unique_state_hash
You should then use the `code` to request an access token.
>**Important:**
-It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
-validate that value is returned and matches in the redirect request.
-This is important to prevent [CSFR attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
-`state` really should have been a requirement in the standard!
+It is highly recommended that you send a `state` value with the request to `/oauth/authorize` and
+validate that value is returned and matches in the redirect request.
+This is important to prevent [CSRF attacks](http://www.oauthsecurity.com/#user-content-authorization-code-flow),
+`state` really should have been a requirement in the standard!
### 3. Requesting the access token
@@ -62,7 +62,7 @@ RestClient.post 'http://localhost:3000/oauth/token', parameters
# The response will be
{
"access_token": "de6780bc506a0446309bd9362820ba8aed28aa506c71eedbe1c5c4f9dd350e54",
- "token_type": "bearer",
+ "token_type": "bearer",
"expires_in": 7200,
"refresh_token": "8257e65c97202ed1726cf9571600918f3bffb2544b26e00a61df9897668c33a1"
}
@@ -95,7 +95,7 @@ curl --header "Authorization: Bearer OAUTH-TOKEN" https://localhost:3000/api/v3/
---
-In this flow, a token is requested in exchange for the resource owner credentials (username and password).
+In this flow, a token is requested in exchange for the resource owner credentials (username and password).
The credentials should only be used when there is a high degree of trust between the resource owner and the client (e.g. the
client is part of the device operating system or a highly privileged application), and when other authorization grant types are not
available (such as an authorization code).
@@ -112,7 +112,7 @@ You can do POST request to `/oauth/token` with parameters:
{
"grant_type" : "password",
"username" : "user@example.com",
- "password" : "sekret"
+ "password" : "secret"
}
```
@@ -130,8 +130,8 @@ For testing you can use the oauth2 ruby gem:
```
client = OAuth2::Client.new('the_client_id', 'the_client_secret', :site => "http://example.com")
-access_token = client.password.get_token('user@example.com', 'sekret')
+access_token = client.password.get_token('user@example.com', 'secret')
puts access_token.token
```
-[personal access tokens]: ./README.md#personal-access-tokens
+[personal access tokens]: ./README.md#personal-access-tokens \ No newline at end of file
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 750ce1508df..27436a052da 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -20,7 +20,7 @@ Constants for project visibility levels are next:
## List projects
-Get a list of projects accessible by the authenticated user.
+Get a list of projects for which the authenticated user is a member.
```
GET /projects
@@ -28,11 +28,14 @@ GET /projects
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
```json
[
@@ -153,6 +156,138 @@ Parameters:
]
```
+Get a list of projects which the authenticated user can see.
+
+```
+GET /projects/visible
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
+| `simple` | boolean | no | Return only the ID, URL, name, and path of each project |
+
+```json
+[
+ {
+ "id": 4,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:diaspora/diaspora-client.git",
+ "http_url_to_repo": "http://example.com/diaspora/diaspora-client.git",
+ "web_url": "http://example.com/diaspora/diaspora-client",
+ "tag_list": [
+ "example",
+ "disapora client"
+ ],
+ "owner": {
+ "id": 3,
+ "name": "Diaspora",
+ "created_at": "2013-09-30T13:46:02Z"
+ },
+ "name": "Diaspora Client",
+ "name_with_namespace": "Diaspora / Diaspora Client",
+ "path": "diaspora-client",
+ "path_with_namespace": "diaspora/diaspora-client",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13:46:02Z",
+ "description": "",
+ "id": 3,
+ "name": "Diaspora",
+ "owner_id": 1,
+ "path": "diaspora",
+ "updated_at": "2013-09-30T13:46:02Z"
+ },
+ "archived": false,
+ "avatar_url": "http://example.com/uploads/project/avatar/4/uploads/avatar.png",
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true,
+ "shared_with_groups": []
+ },
+ {
+ "id": 6,
+ "description": null,
+ "default_branch": "master",
+ "public": false,
+ "visibility_level": 0,
+ "ssh_url_to_repo": "git@example.com:brightbox/puppet.git",
+ "http_url_to_repo": "http://example.com/brightbox/puppet.git",
+ "web_url": "http://example.com/brightbox/puppet",
+ "tag_list": [
+ "example",
+ "puppet"
+ ],
+ "owner": {
+ "id": 4,
+ "name": "Brightbox",
+ "created_at": "2013-09-30T13:46:02Z"
+ },
+ "name": "Puppet",
+ "name_with_namespace": "Brightbox / Puppet",
+ "path": "puppet",
+ "path_with_namespace": "brightbox/puppet",
+ "issues_enabled": true,
+ "open_issues_count": 1,
+ "merge_requests_enabled": true,
+ "builds_enabled": true,
+ "wiki_enabled": true,
+ "snippets_enabled": false,
+ "container_registry_enabled": false,
+ "created_at": "2013-09-30T13:46:02Z",
+ "last_activity_at": "2013-09-30T13:46:02Z",
+ "creator_id": 3,
+ "namespace": {
+ "created_at": "2013-09-30T13:46:02Z",
+ "description": "",
+ "id": 4,
+ "name": "Brightbox",
+ "owner_id": 1,
+ "path": "brightbox",
+ "updated_at": "2013-09-30T13:46:02Z"
+ },
+ "permissions": {
+ "project_access": {
+ "access_level": 10,
+ "notification_level": 3
+ },
+ "group_access": {
+ "access_level": 50,
+ "notification_level": 3
+ }
+ },
+ "archived": false,
+ "avatar_url": null,
+ "shared_runners_enabled": true,
+ "forks_count": 0,
+ "star_count": 0,
+ "runners_token": "b8547b1dc37721d05889db52fa2f02",
+ "public_builds": true,
+ "shared_with_groups": []
+ }
+]
+```
+
### List owned projects
Get a list of projects which are owned by the authenticated user.
@@ -163,11 +298,13 @@ GET /projects/owned
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### List starred projects
@@ -179,11 +316,13 @@ GET /projects/starred
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### List ALL projects
@@ -195,11 +334,13 @@ GET /projects/all
Parameters:
-- `archived` (optional) - if passed, limit by archived status
-- `visibility` (optional) - if passed, limit by visibility `public`, `internal`, `private`
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `path`, `created_at`, `updated_at` or `last_activity_at` fields. Default is `created_at`
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order. Default is `desc`
-- `search` (optional) - Return list of authorized projects according to a search criteria
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `archived` | boolean | no | Limit by archived status |
+| `visibility` | string | no | Limit by visibility `public`, `internal`, or `private` |
+| `order_by` | string | no | Return projects ordered by `id`, `name`, `path`, `created_at`, `updated_at`, or `last_activity_at` fields. Default is `created_at` |
+| `sort` | string | no | Return projects sorted in `asc` or `desc` order. Default is `desc` |
+| `search` | string | no | Return list of authorized projects matching the search criteria |
### Get single project
@@ -212,7 +353,9 @@ GET /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```json
{
@@ -301,7 +444,9 @@ GET /projects/:id/events
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```json
[
@@ -439,24 +584,26 @@ POST /projects
Parameters:
-- `name` (required) - new project name
-- `path` (optional) - custom repository name for new project. By default generated based on name
-- `namespace_id` (optional) - namespace for the new project (defaults to user)
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
### Create project for user
@@ -468,23 +615,27 @@ POST /projects/user/:user_id
Parameters:
-- `user_id` (required) - user_id of owner
-- `name` (required) - new project name
-- `description` (optional) - short project description
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `import_url` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `user_id` | integer | yes | The user ID of the project owner |
+| `name` | string | yes | The name of the new project |
+| `path` | string | no | Custom repository name for new project. By default generated based on name |
+| `namespace_id` | integer | no | Namespace for the new project (defaults to the current user's namespace) |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
### Edit project
@@ -496,24 +647,26 @@ PUT /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `name` (optional) - project name
-- `path` (optional) - repository name for project
-- `description` (optional) - short project description
-- `default_branch` (optional)
-- `issues_enabled` (optional)
-- `merge_requests_enabled` (optional)
-- `builds_enabled` (optional)
-- `wiki_enabled` (optional)
-- `snippets_enabled` (optional)
-- `container_registry_enabled` (optional)
-- `shared_runners_enabled` (optional)
-- `public` (optional) - if `true` same as setting visibility_level = 20
-- `visibility_level` (optional)
-- `public_builds` (optional)
-- `only_allow_merge_if_build_succeeds` (optional)
-- `lfs_enabled` (optional)
-- `request_access_enabled` (optional) - Allow users to request member access.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `name` | string | yes | The name of the project |
+| `path` | string | no | Custom repository name for the project. By default generated based on name |
+| `description` | string | no | Short project description |
+| `issues_enabled` | boolean | no | Enable issues for this project |
+| `merge_requests_enabled` | boolean | no | Enable merge requests for this project |
+| `builds_enabled` | boolean | no | Enable builds for this project |
+| `wiki_enabled` | boolean | no | Enable wiki for this project |
+| `snippets_enabled` | boolean | no | Enable snippets for this project |
+| `container_registry_enabled` | boolean | no | Enable container registry for this project |
+| `shared_runners_enabled` | boolean | no | Enable shared runners for this project |
+| `public` | boolean | no | If `true`, the same as setting `visibility_level` to 20 |
+| `visibility_level` | integer | no | See [project visibility level][#project-visibility-level] |
+| `import_url` | string | no | URL to import repository from |
+| `public_builds` | boolean | no | If `true`, builds can be viewed by non-project-members |
+| `only_allow_merge_if_build_succeeds` | boolean | no | Set whether merge requests can only be merged with successful builds |
+| `lfs_enabled` | boolean | no | Enable LFS |
+| `request_access_enabled` | boolean | no | Allow users to request member access |
On success, method returns 200 with the updated project. If parameters are
invalid, 400 is returned.
@@ -528,8 +681,10 @@ POST /projects/fork/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `namespace` (optional) - The ID or path of the namespace that the project will be forked to
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
+| `namespace` | integer/string | yes | The ID or path of the namespace that the project will be forked to |
### Star a project
@@ -540,9 +695,11 @@ Stars a given project. Returns status code `201` and the project on success and
POST /projects/:id/star
```
+Parameters:
+
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID or NAMESPACE/PROJECT_NAME of the project |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -610,7 +767,7 @@ DELETE /projects/:id/star
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/5/star"
@@ -682,7 +839,7 @@ POST /projects/:id/archive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/archive"
@@ -770,7 +927,7 @@ POST /projects/:id/unarchive
| Attribute | Type | Required | Description |
| --------- | ---- | -------- | ----------- |
-| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```bash
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v3/projects/unarchive"
@@ -853,7 +1010,9 @@ DELETE /projects/:id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
## Uploads
@@ -867,8 +1026,10 @@ POST /projects/:id/uploads
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `file` (required) - The file to be uploaded
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `file` | string | yes | The file to be uploaded |
```json
{
@@ -896,9 +1057,12 @@ POST /projects/:id/share
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `group_id` (required) - The ID of a group
-- `group_access` (required) - Level of permissions for sharing
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `group_id` | integer | yes | The ID of the group to share with |
+| `group_access` | integer | yes | The permissions level to grant the group |
+| `expires_at` | string | no | Share expiration date in ISO 8601 format: 2016-09-26 |
## Hooks
@@ -915,7 +1079,9 @@ GET /projects/:id/hooks
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
### Get project hook
@@ -927,8 +1093,10 @@ GET /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of a project hook |
```json
{
@@ -958,17 +1126,19 @@ POST /projects/:id/hooks
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `build_events` - Trigger hook on build events
-- `pipeline_events` - Trigger hook on pipeline events
-- `wiki_page_events` - Trigger hook on wiki page events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on 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 |
+| `build_events` | boolean | no | Trigger hook on build events |
+| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
+| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
### Edit project hook
@@ -980,18 +1150,20 @@ PUT /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of a project hook
-- `url` (required) - The hook URL
-- `push_events` - Trigger hook on push events
-- `issues_events` - Trigger hook on issues events
-- `merge_requests_events` - Trigger hook on merge_requests events
-- `tag_push_events` - Trigger hook on push_tag events
-- `note_events` - Trigger hook on note events
-- `build_events` - Trigger hook on build events
-- `pipeline_events` - Trigger hook on pipeline events
-- `wiki_page_events` - Trigger hook on wiki page events
-- `enable_ssl_verification` - Do SSL verification when triggering the hook
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
+| `url` | string | yes | The hook URL |
+| `push_events` | boolean | no | Trigger hook on push events |
+| `issues_events` | boolean | no | Trigger hook on 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 |
+| `build_events` | boolean | no | Trigger hook on build events |
+| `pipeline_events` | boolean | no | Trigger hook on pipeline events |
+| `wiki_events` | boolean | no | Trigger hook on wiki events |
+| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
### Delete project hook
@@ -1004,8 +1176,10 @@ DELETE /projects/:id/hooks/:hook_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `hook_id` (required) - The ID of hook to delete
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `hook_id` | integer | yes | The ID of the project hook |
Note the JSON response differs if the hook is available or not. If the project hook
is available before it is returned in the JSON response or an empty response is returned.
@@ -1024,7 +1198,9 @@ GET /projects/:id/repository/branches
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
```json
[
@@ -1079,10 +1255,12 @@ GET /projects/:id/repository/branches/:branch
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
-- `developers_can_push` - Flag if developers can push to the branch.
-- `developers_can_merge` - Flag if developers can merge to the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
+| `developers_can_push` | boolean | no | Flag if developers can push to the branch |
+| `developers_can_merge` | boolean | no | Flag if developers can merge to the branch |
### Protect single branch
@@ -1094,8 +1272,10 @@ PUT /projects/:id/repository/branches/:branch/protect
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
### Unprotect single branch
@@ -1107,8 +1287,10 @@ PUT /projects/:id/repository/branches/:branch/unprotect
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of a project
-- `branch` (required) - The name of the branch.
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `branch` | string | yes | The name of the branch |
## Admin fork relation
@@ -1122,8 +1304,10 @@ POST /projects/:id/fork/:forked_from_id
Parameters:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
-- `forked_from_id:` (required) - The ID of the project that was forked from
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
+| `forked_from_id` | ID | yes | The ID of the project that was forked from |
### Delete an existing forked from relationship
@@ -1133,7 +1317,9 @@ DELETE /projects/:id/fork
Parameter:
-- `id` (required) - The ID or NAMESPACE/PROJECT_NAME of the project to be forked
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID of the project or NAMESPACE/PROJECT_NAME |
## Search for projects by name
@@ -1145,8 +1331,10 @@ GET /projects/search/:query
Parameters:
-- `query` (required) - A string contained in the project name
-- `per_page` (optional) - number of projects to return per page
-- `page` (optional) - the page to retrieve
-- `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
-- `sort` (optional) - Return requests sorted in `asc` or `desc` order
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `query` (required) - A string contained in the project name
+| `per_page` (optional) - number of projects to return per page
+| `page` (optional) - the page to retrieve
+| `order_by` (optional) - Return requests ordered by `id`, `name`, `created_at` or `last_activity_at` fields
+| `sort` | string | no | Return requests sorted in `asc` or `desc` order |
diff --git a/doc/api/settings.md b/doc/api/settings.md
index aaa2c99642b..f7ad3b4cc8e 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -41,7 +41,9 @@ Example response:
"gravatar_enabled" : true,
"sign_in_text" : null,
"container_registry_token_expire_delay": 5,
- "repository_storage": "default"
+ "repository_storage": "default",
+ "koding_enabled": false,
+ "koding_url": null
}
```
@@ -72,7 +74,9 @@ PUT /application/settings
| `after_sign_out_path` | string | no | Where to redirect users after logout |
| `container_registry_token_expire_delay` | integer | no | Container Registry token duration in minutes |
| `repository_storage` | string | no | Storage path for new projects. The value should be the name of one of the repository storage paths defined in your gitlab.yml |
-| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols.
+| `enabled_git_access_protocol` | string | no | Enabled protocols for Git access. Allowed values are: `ssh`, `http`, and `nil` to allow both protocols. |
+| `koding_enabled` | boolean | no | Enable Koding integration. Default is `false`. |
+| `koding_url` | string | yes (if `koding_enabled` is `true`) | The Koding instance URL for integration. |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v3/application/settings?signup_enabled=false&default_project_visibility=1
@@ -103,6 +107,8 @@ Example response:
"user_oauth_applications": true,
"after_sign_out_path": "",
"container_registry_token_expire_delay": 5,
- "repository_storage": "default"
+ "repository_storage": "default",
+ "koding_enabled": false,
+ "koding_url": null
}
```
diff --git a/doc/api/users.md b/doc/api/users.md
index 54f7a2a2ace..9be4f2e6ec3 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -57,6 +57,7 @@ GET /users
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
@@ -89,6 +90,7 @@ GET /users
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
"theme_id": 1,
@@ -147,7 +149,8 @@ Parameters:
"skype": "",
"linkedin": "",
"twitter": "",
- "website_url": ""
+ "website_url": "",
+ "organization": ""
}
```
@@ -178,6 +181,7 @@ Parameters:
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
@@ -214,6 +218,7 @@ Parameters:
- `linkedin` (optional) - LinkedIn
- `twitter` (optional) - Twitter account
- `website_url` (optional) - Website URL
+- `organization` (optional) - Organization name
- `projects_limit` (optional) - Number of projects user can create
- `extern_uid` (optional) - External UID
- `provider` (optional) - External provider name
@@ -242,6 +247,7 @@ Parameters:
- `linkedin` - LinkedIn
- `twitter` - Twitter account
- `website_url` - Website URL
+- `organization` - Organization name
- `projects_limit` - Limit projects each user can create
- `extern_uid` - External UID
- `provider` - External provider name
@@ -296,6 +302,7 @@ GET /user
"linkedin": "",
"twitter": "",
"website_url": "",
+ "organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
"theme_id": 1,
diff --git a/doc/ci/docker/using_docker_images.md b/doc/ci/docker/using_docker_images.md
index a849905ac6b..520c8b36a95 100644
--- a/doc/ci/docker/using_docker_images.md
+++ b/doc/ci/docker/using_docker_images.md
@@ -221,7 +221,7 @@ time.
*Note: The following commands are run without root privileges. You should be
able to run docker with your regular user account.*
-First start with creating a file named `build script`:
+First start with creating a file named `build_script`:
```bash
cat <<EOF > build_script
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index d85b8a34ced..e070302fb82 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -14,6 +14,19 @@ Defining environments in a project's `.gitlab-ci.yml` lets developers track
Deployments are created when [jobs] deploy versions of code to [environments].
+### Checkout deployments locally
+
+Since 8.13, a reference in the git repository is saved for each deployment. So
+knowing what the state is of your current environments is only a `git fetch`
+away.
+
+In your git config, append the `[remote "<your-remote>"]` block with an extra
+fetch line:
+
+```
+fetch = +refs/environments/*:refs/remotes/origin/environments/*
+```
+
## Defining environments
You can create and delete environments manually in the web interface, but we
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 71670e6247c..08fbd9afa2f 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -11,9 +11,10 @@ Apart from those, here is an collection of tutorials and guides on setting up yo
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
- [Test a Scala application](test-scala-application.md)
+- [Test a Phoenix application](test-phoenix-application.md)
- [Using `dpl` as deployment tool](deployment/README.md)
- [Blog post about using GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
-- [Repo's with examples for various languages](https://gitlab.com/groups/gitlab-examples)
+- [Repositories with examples for various languages](https://gitlab.com/groups/gitlab-examples)
- [The .gitlab-ci.yml file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
[gitlab-ci-templates]: https://gitlab.com/gitlab-org/gitlab-ci-yml
diff --git a/doc/ci/examples/test-phoenix-application.md b/doc/ci/examples/test-phoenix-application.md
new file mode 100644
index 00000000000..150698ca04b
--- /dev/null
+++ b/doc/ci/examples/test-phoenix-application.md
@@ -0,0 +1,56 @@
+## Test a Phoenix application
+
+This example demonstrates the integration of Gitlab CI with Phoenix, Elixir and
+Postgres.
+
+### Add `.gitlab-ci.yml` file to project
+
+The following `.gitlab-ci.yml` should be added in the root of your
+repository to trigger CI:
+
+```yaml
+image: elixir:1.3
+
+services:
+ - postgres:9.6
+
+variables:
+ MIX_ENV: "test"
+
+before_script:
+ # Setup phoenix dependencies
+ - apt-get update
+ - apt-get install -y postgresql-client
+ - mix local.hex --force
+ - mix deps.get --only test
+ - mix ecto.reset
+
+test:
+ script:
+ - mix test
+```
+
+The variables will set the Mix environment to "test". The
+`before_script` will install `psql`, some Phoenix dependencies, and will also
+run your migrations.
+
+Finally, the test `script` will run your tests.
+
+### Update the Config Settings
+
+In `config/test.exs`, update the database hostname:
+
+```elixir
+config :my_app, MyApp.Repo,
+ hostname: if(System.get_env("CI"), do: "postgres", else: "localhost"),
+```
+
+### Add the Migrations Folder
+
+If you do not have any migrations yet, you will need to create an empty
+`.gitkeep` file in `priv/repo/migrations`.
+
+### Sources
+
+- https://medium.com/@nahtnam/using-phoenix-on-gitlab-ci-5a51eec81142
+- https://davejlong.com/ci-with-phoenix-and-gitlab/
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
index ca9b986a060..729c1dc8c0d 100644
--- a/doc/ci/pipelines.md
+++ b/doc/ci/pipelines.md
@@ -31,6 +31,8 @@ project.
## Seeing build status
Clicking on a pipeline will show the builds that were run for that pipeline.
+Clicking on an individual build will show you its build trace, and allow you to
+cancel the build, retry it, or erase the build trace.
## Badges
diff --git a/doc/ci/triggers/README.md b/doc/ci/triggers/README.md
index b78422f6d0e..84048f1d25f 100644
--- a/doc/ci/triggers/README.md
+++ b/doc/ci/triggers/README.md
@@ -4,7 +4,7 @@
> **Note**:
GitLab 8.12 has a completely redesigned build permissions system.
-Read all about the [new model and its implications][../../user/project/new_ci_build_permissions_model.md#build-triggers].
+Read all about the [new model and its implications](../../user/project/new_ci_build_permissions_model.md#build-triggers).
Triggers can be used to force a rebuild of a specific branch, tag or commit,
with an API call.
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 6a971c3ae87..a4c3a731a20 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -44,10 +44,11 @@ The `API_TOKEN` will take the Secure Variable value: `SECURE`.
| **CI_PROJECT_URL** | 8.10 | 0.5 | The HTTP address to access project |
| **CI_PROJECT_DIR** | all | all | The full path where the repository is cloned and where the build is run |
| **CI_REGISTRY** | 8.10 | 0.5 | If the Container Registry is enabled it returns the address of GitLab's Container Registry |
-| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returnes the address of the registry tied to the specific project |
+| **CI_REGISTRY_IMAGE** | 8.10 | 0.5 | If the Container Registry is enabled for the project it returns the address of the registry tied to the specific project |
| **CI_RUNNER_ID** | 8.10 | 0.5 | The unique id of runner being used |
| **CI_RUNNER_DESCRIPTION** | 8.10 | 0.5 | The description of the runner as saved in GitLab |
| **CI_RUNNER_TAGS** | 8.10 | 0.5 | The defined runner tags |
+| **CI_DEBUG_TRACE** | all | 1.7 | Whether [debug tracing](#debug-tracing) is enabled |
| **GITLAB_USER_ID** | 8.12 | all | The id of the user who started the build |
| **GITLAB_USER_EMAIL** | 8.12 | all | The email of the user who started the build |
@@ -105,6 +106,39 @@ Variables can be defined at a global level, but also at a job level.
More information about Docker integration can be found in [Using Docker Images](../docker/using_docker_images.md).
+#### Debug tracing
+
+> **WARNING:** Enabling debug tracing can have severe security implications. The
+ output **will** contain the content of all your secure variables and any other
+ secrets! The output **will** be uploaded to the GitLab server and made visible
+ in build traces!
+
+By default, GitLab Runner hides most of the details of what it is doing when
+processing a job. This behaviour keeps build traces short, and prevents secrets
+from being leaked into the trace unless your script writes them to the screen.
+
+If a job isn't working as expected, this can make the problem difficult to
+investigate; in these cases, you can enable debug tracing in `.gitlab-ci.yml`.
+Available on GitLab Runner v1.7+, this feature enables the shell's execution
+trace, resulting in a verbose build trace listing all commands that were run,
+variables that were set, etc.
+
+Before enabling this, you should ensure builds are visible to
+[team members only](../../../user/permissions.md#project-features). You should
+also [erase](../pipelines.md#seeing-build-traces) all generated build traces
+before making them visible again.
+
+To enable debug traces, set the `CI_DEBUG_TRACE` variable to `true`:
+
+```yaml
+job1:
+ variables:
+ CI_DEBUG_TRACE: "true"
+```
+
+The [example project](https://gitlab.com/gitlab-examples/ci-debug-trace)
+demonstrates a working configuration, including build trace examples.
+
### User-defined variables (Secure Variables)
**This feature requires GitLab Runner 0.4.0 or higher**
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 16868554c1f..cdf5ecc7a84 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -858,27 +858,45 @@ job:
## Git Strategy
-> Introduced in GitLab 8.9 as an experimental feature. May change in future
- releases or be removed completely.
+> Introduced in GitLab 8.9 as an experimental feature. May change or be removed
+ completely in future releases. `GIT_STRATEGY=none` requires GitLab Runner
+ v1.7+.
+
+You can set the `GIT_STRATEGY` used for getting recent application code, either
+in the global [`variables`](#variables) section or the [`variables`](#job-variables)
+section for individual jobs. If left unspecified, the default from project
+settings will be used.
-You can set the `GIT_STRATEGY` used for getting recent application code. `clone`
-is slower, but makes sure you have a clean directory before every build. `fetch`
-is faster. `GIT_STRATEGY` can be specified in the global `variables` section or
-in the `variables` section for individual jobs. If it's not specified, then the
-default from project settings will be used.
+There are three possible values: `clone`, `fetch`, and `none`.
+
+`clone` is the slowest option. It clones the repository from scratch for every
+job, ensuring that the project workspace is always pristine.
```
variables:
GIT_STRATEGY: clone
```
-or
+`fetch` is faster as it re-uses the project workspace (falling back to `clone`
+if it doesn't exist). `git clean` is used to undo any changes made by the last
+job, and `git fetch` is used to retrieve commits made since the last job ran.
```
variables:
GIT_STRATEGY: fetch
```
+`none` also re-uses the project workspace, but skips all Git operations
+(including GitLab Runner's pre-clone script, if present). It is mostly useful
+for jobs that operate exclusively on artifacts (e.g., `deploy`). Git repository
+data may be present, but it is certain to be out of date, so you should only
+rely on files brought into the project workspace from cache or artifacts.
+
+```
+variables:
+ GIT_STRATEGY: none
+```
+
## Shallow cloning
> Introduced in GitLab 8.9 as an experimental feature. May change in future
diff --git a/doc/container_registry/README.md b/doc/container_registry/README.md
index d7740647a91..fe3e4681ba7 100644
--- a/doc/container_registry/README.md
+++ b/doc/container_registry/README.md
@@ -1,98 +1 @@
-# GitLab Container Registry
-
-> [Introduced][ce-4040] in GitLab 8.8. Docker Registry manifest
-`v1` support was added in GitLab 8.9 to support Docker versions earlier than 1.10.
-
-> **Note:**
-This document is about the user guide. To learn how to enable GitLab Container
-Registry across your GitLab instance, visit the
-[administrator documentation](../administration/container_registry.md).
-
-With the Docker Container Registry integrated into GitLab, every project can
-have its own space to store its Docker images.
-
-You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
-
----
-
-## Enable the Container Registry for your project
-
-1. First, ask your system administrator to enable GitLab Container Registry
- following the [administration documentation](../administration/container_registry.md).
- If you are using GitLab.com, this is enabled by default so you can start using
- the Registry immediately.
-
-1. Go to your project's settings and enable the **Container Registry** feature
- on your project. For new projects this might be enabled by default. For
- existing projects you will have to explicitly enable it.
-
- ![Enable Container Registry](img/project_feature.png)
-
-## Build and push images
-
-After you save your project's settings, you should see a new link in the
-sidebar called **Container Registry**. Following this link will get you to
-your project's Registry panel where you can see how to login to the Container
-Registry using your GitLab credentials.
-
-For example if the Registry's URL is `registry.example.com`, the you should be
-able to login with:
-
-```
-docker login registry.example.com
-```
-
-Building and publishing images should be a straightforward process. Just make
-sure that you are using the Registry URL with the namespace and project name
-that is hosted on GitLab:
-
-```
-docker build -t registry.example.com/group/project .
-docker push registry.example.com/group/project
-```
-
-## Use images from GitLab Container Registry
-
-To download and run a container from images hosted in GitLab Container Registry,
-use `docker run`:
-
-```
-docker run [options] registry.example.com/group/project [arguments]
-```
-
-For more information on running Docker containers, visit the
-[Docker documentation][docker-docs].
-
-## Control Container Registry from within GitLab
-
-GitLab offers a simple Container Registry management panel. Go to your project
-and click **Container Registry** in the left sidebar.
-
-This view will show you all tags in your project and will easily allow you to
-delete them.
-
-![Container Registry panel](img/container_registry.png)
-
-## Build and push images using GitLab CI
-
-> **Note:**
-This feature requires GitLab 8.8 and GitLab Runner 1.2.
-
-Make sure that your GitLab Runner is configured to allow building Docker images by
-following the [Using Docker Build](../ci/docker/using_docker_build.md)
-and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
-
-## Limitations
-
-In order to use a container image from your private project as an `image:` in
-your `.gitlab-ci.yml`, you have to follow the
-[Using a private Docker Registry][private-docker]
-documentation. This workflow will be simplified in the future.
-
-## Troubleshooting
-
-See [the GitLab Docker registry troubleshooting guide](troubleshooting.md).
-
-[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
-[docker-docs]: https://docs.docker.com/engine/userguide/intro/
-[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
+This document was moved in [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/container_registry/img/container_registry.png b/doc/container_registry/img/container_registry.png
deleted file mode 100644
index 57d6f9f22c5..00000000000
--- a/doc/container_registry/img/container_registry.png
+++ /dev/null
Binary files differ
diff --git a/doc/container_registry/img/project_feature.png b/doc/container_registry/img/project_feature.png
deleted file mode 100644
index a59b4f82b56..00000000000
--- a/doc/container_registry/img/project_feature.png
+++ /dev/null
Binary files differ
diff --git a/doc/container_registry/troubleshooting.md b/doc/container_registry/troubleshooting.md
index 14c4a7d9a63..2f8cd37b488 100644
--- a/doc/container_registry/troubleshooting.md
+++ b/doc/container_registry/troubleshooting.md
@@ -1,141 +1 @@
-# Troubleshooting the GitLab Container Registry
-
-## Basic Troubleshooting
-
-1. Check to make sure that the system clock on your Docker client and GitLab server have
- been synchronized (e.g. via NTP).
-
-2. If you are using an S3-backed Registry, double check that the IAM
- permissions and the S3 credentials (including region) are correct. See [the
- sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
- for more details.
-
-3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
- for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
- there.
-
-## Advanced Troubleshooting
-
->**NOTE:** The following section is only recommended for experts.
-
-Sometimes it's not obvious what is wrong, and you may need to dive deeper into
-the communication between the Docker client and the Registry to find out
-what's wrong. We will use a concrete example in the past to illustrate how to
-diagnose a problem with the S3 setup.
-
-### Unexpected 403 error during push
-
-A user attempted to enable an S3-backed Registry. The `docker login` step went
-fine. However, when pushing an image, the output showed:
-
-```
-The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
-dc5e59c14160: Pushing [==================================================>] 14.85 kB
-03c20c1a019a: Pushing [==================================================>] 2.048 kB
-a08f14ef632e: Pushing [==================================================>] 2.048 kB
-228950524c88: Pushing 2.048 kB
-6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
-5f70bf18a086: Pushing 1.024 kB
-737f40e80b7f: Waiting
-82b57dbc5385: Waiting
-19429b698a22: Waiting
-9436069b92a3: Waiting
-error parsing HTTP 403 response body: unexpected end of JSON input: ""
-```
-
-This error is ambiguous, as it's not clear whether the 403 is coming from the
-GitLab Rails application, the Docker Registry, or something else. In this
-case, since we know that since the login succeeded, we probably need to look
-at the communication between the client and the Registry.
-
-The REST API between the Docker client and Registry is [described
-here](https://docs.docker.com/registry/spec/api/). Normally, one would just
-use Wireshark or tcpdump to capture the traffic and see where things went
-wrong. However, since all communication between Docker clients and servers
-are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
-if you know the private key. What can we do instead?
-
-One way would be to disable HTTPS by setting up an [insecure
-Registry](https://docs.docker.com/registry/insecure/). This could introduce a
-security hole and is only recommended for local testing. If you have a
-production system and can't or don't want to do this, there is another way:
-use mitmproxy, which stands for Man-in-the-Middle Proxy.
-
-### mitmproxy
-
-[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
-client and server to inspect all traffic. One wrinkle is that your system
-needs to trust the mitmproxy SSL certificates for this to work.
-
-The following installation instructions assume you are running Ubuntu:
-
-1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
-1. Run `mitmproxy --port 9000` to generate its certificates.
- Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
-1. Install the certificate from `~/.mitmproxy` to your system:
-
- ```sh
- sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
- sudo update-ca-certificates
- ```
-
-If successful, the output should indicate that a certificate was added:
-
-```sh
-Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
-Running hooks in /etc/ca-certificates/update.d....done.
-```
-
-To verify that the certificates are properly installed, run:
-
-```sh
-mitmproxy --port 9000
-```
-
-This will run mitmproxy on port `9000`. In another window, run:
-
-```sh
-curl --proxy http://localhost:9000 https://httpbin.org/status/200
-```
-
-If everything is setup correctly, you will see information on the mitmproxy window and
-no errors from the curl commands.
-
-### Running the Docker daemon with a proxy
-
-For Docker to connect through a proxy, you must start the Docker daemon with the
-proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
-and then run Docker by hand. As root, run:
-
-```sh
-export HTTP_PROXY="http://localhost:9000"
-export HTTPS_PROXY="https://localhost:9000"
-docker daemon --debug
-```
-
-This will launch the Docker daemon and proxy all connections through mitmproxy.
-
-### Running the Docker client
-
-Now that we have mitmproxy and Docker running, we can attempt to login and push
-a container image. You may need to run as root to do this. For example:
-
-```sh
-docker login s3-testing.myregistry.com:4567
-docker push s3-testing.myregistry.com:4567/root/docker-test
-```
-
-In the example above, we see the following trace on the mitmproxy window:
-
-![mitmproxy output from Docker](img/mitmproxy-docker.png)
-
-The above image shows:
-
-* The initial PUT requests went through fine with a 201 status code.
-* The 201 redirected the client to the S3 bucket.
-* The HEAD request to the AWS bucket reported a 403 Unauthorized.
-
-What does this mean? This strongly suggests that the S3 user does not have the right
-[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
-The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
-Once the right permissions were set, the error will go away.
+This document was moved to [user/project/container_registry](../user/project/container_registry.md).
diff --git a/doc/development/README.md b/doc/development/README.md
index 58c00f618fa..9706cb1de7f 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -13,6 +13,7 @@
- [SQL Migration Style Guide](migration_style_guide.md) for creating safe SQL migrations
- [Testing standards and style guidelines](testing.md)
- [UI guide](ui_guide.md) for building GitLab with existing CSS styles and elements
+- [Frontend guidelines](frontend.md)
- [SQL guidelines](sql.md) for SQL guidelines
## Process
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 40ae55ab905..c5c23b5c0b8 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -34,6 +34,10 @@ request is up to one of our merge request "endbosses", denoted on the
## Having your code reviewed
+Please keep in mind that code review is a process that can take multiple
+iterations, and reviewers may spot things later that they may not have seen the
+first time.
+
- The first reviewer of your code is _you_. Before you perform that first push
of your shiny new branch, read through the entire diff. Does it make sense?
Did you include something unrelated to the overall purpose of the changes? Did
@@ -55,6 +59,7 @@ request is up to one of our merge request "endbosses", denoted on the
Understand why the change is necessary (fixes a bug, improves the user
experience, refactors the existing code). Then:
+- Try to be thorough in your reviews to reduce the number of iterations.
- Communicate which ideas you feel strongly about and those you don't.
- Identify ways to simplify the code while still solving the problem.
- Offer alternative implementations, but assume the author already considered
@@ -64,8 +69,10 @@ experience, refactors the existing code). Then:
someone else would be confused by it as well.
- After a round of line notes, it can be helpful to post a summary note such as
"LGTM :thumbsup:", or "Just a couple things to address."
-- Avoid accepting a merge request before the build succeeds ("Merge when build
- succeeds" is fine).
+- Avoid accepting a merge request before the build succeeds. Of course, "Merge
+ When Build Succeeds" (MWBS) is fine.
+- If you set the MR to "Merge When Build Succeeds", you should take over
+ subsequent revisions for anything that would be spotted after that.
## Credits
diff --git a/doc/development/frontend.md b/doc/development/frontend.md
new file mode 100644
index 00000000000..f879cd57e25
--- /dev/null
+++ b/doc/development/frontend.md
@@ -0,0 +1,225 @@
+# Frontend Development Guidelines
+
+This document describes various guidelines to ensure consistency and quality
+across GitLab's frontend team.
+
+## Overview
+
+GitLab is built on top of [Ruby on Rails][rails] using [Haml][haml] with
+[Hamlit][hamlit]. Be wary of [the limitations that come with using
+Hamlit][hamlit-limits]. We also use [SCSS][scss] and plain JavaScript with
+[ES6 by way of Babel][es6].
+
+The asset pipeline is [Sprockets][sprockets], which handles the concatenation,
+minification, and compression of our assets.
+
+[jQuery][jquery] is used throughout the application's JavaScript, with
+[Vue.js][vue] for particularly advanced, dynamic elements.
+
+### Vue
+
+For more complex frontend features, we recommend using Vue.js. It shares
+some ideas with React.js as well as Angular.
+
+To get started with Vue, read through [their documentation][vue-docs].
+
+## Performance
+
+### Resources
+
+- [WebPage Test][web-page-test] for testing site loading time and size.
+- [Google PageSpeed Insights][pagespeed-insights] grades web pages and provides feedback to improve the page.
+- [Profiling with Chrome DevTools][google-devtools-profiling]
+- [Browser Diet][browser-diet] is a community-built guide that catalogues practical tips for improving web page performance.
+
+### Page-specific JavaScript
+
+Certain pages may require the use of a third party library, such as [d3][d3] for
+the User Activity Calendar and [Chart.js][chartjs] for the Graphs pages. These
+libraries increase the page size significantly, and impact load times due to
+bandwidth bottlenecks and the browser needing to parse more JavaScript.
+
+In cases where libraries are only used on a few specific pages, we use
+"page-specific JavaScript" to prevent the main `application.js` file from
+becoming unnecessarily large.
+
+Steps to split page-specific JavaScript from the main `application.js`:
+
+1. Create a directory for the specific page(s), e.g. `graphs/`.
+1. In that directory, create a `namespace_bundle.js` file, e.g. `graphs_bundle.js`.
+1. In `graphs_bundle.js` add the line `//= require_tree .`, this adds all other files in the directory to the bundle.
+1. Add any necessary libraries to `app/assets/javascripts/lib/`, all files directly descendant from this directory will be precompiled as separate assets, in this case `chart.js` would be added.
+1. Add the new "bundle" file to the list of precompiled assets in
+`config/application.rb`.
+ - For example: `config.assets.precompile << "graphs/graphs_bundle.js"`.
+1. Move code reliant on these libraries into the `graphs` directory.
+1. In the relevant views, add the scripts to the page with the following:
+
+```haml
+- content_for :page_specific_javascripts do
+ = page_specific_javascript_tag('lib/chart.js')
+ = page_specific_javascript_tag('graphs/graphs_bundle.js')
+```
+
+The above loads `chart.js` and `graphs_bundle.js` for this page only. `chart.js`
+is separated from the bundle file so it can be cached separately from the bundle
+and reused for other pages that also rely on the library. For an example, see
+[this Haml file][page-specific-js-example].
+
+### Minimizing page size
+
+A smaller page size means the page loads faster (especially important on mobile
+and poor connections), the page is parsed more quickly by the browser, and less
+data is used for users with capped data plans.
+
+General tips:
+
+- Don't add new fonts.
+- Prefer font formats with better compression, e.g. WOFF2 is better than WOFF, which is better than TTF.
+- Compress and minify assets wherever possible (For CSS/JS, Sprockets does 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.
+
+## Accessibility
+
+### Resources
+
+[Chrome Accessibility Developer Tools][chrome-accessibility-developer-tools]
+are useful for testing for potential accessibility problems in GitLab.
+
+Accessibility best-practices and more in-depth information is available on
+[the Audit Rules page][audit-rules] for the Chrome Accessibility Developer Tools.
+
+## Security
+
+### Resources
+
+[Mozilla’s HTTP Observatory CLI][observatory-cli] and the
+[Qualys SSL Labs Server Test][qualys-ssl] are good resources for finding
+potential problems and ensuring compliance with security best practices.
+
+<!-- Uncomment these sections when CSP/SRI are implemented.
+### Content Security Policy (CSP)
+
+Content Security Policy is a web standard that intends to mitigate certain
+forms of Cross-Site Scripting (XSS) as well as data injection.
+
+Content Security Policy rules should be taken into consideration when
+implementing new features, especially those that may rely on connection with
+external services.
+
+GitLab's CSP is used for the following:
+
+- Blocking plugins like Flash and Silverlight from running at all on our pages.
+- Blocking the use of scripts and stylesheets downloaded from external sources.
+- Upgrading `http` requests to `https` when possible.
+- Preventing `iframe` elements from loading in most contexts.
+
+Some exceptions include:
+
+- Scripts from Google Analytics and Piwik if either is enabled.
+- Connecting with GitHub, Bitbucket, GitLab.com, etc. to allow project importing.
+- Connecting with Google, Twitter, GitHub, etc. to allow OAuth authentication.
+
+We use [the Secure Headers gem][secure_headers] to enable Content
+Security Policy headers in the GitLab Rails app.
+
+Some resources on implementing Content Security Policy:
+
+- [MDN Article on CSP][mdn-csp]
+- [GitHub’s CSP Journey on the GitHub Engineering Blog][github-eng-csp]
+- The Dropbox Engineering Blog's series on CSP: [1][dropbox-csp-1], [2][dropbox-csp-2], [3][dropbox-csp-3], [4][dropbox-csp-4]
+
+### Subresource Integrity (SRI)
+
+Subresource Integrity prevents malicious assets from being provided by a CDN by
+guaranteeing that the asset downloaded is identical to the asset the server
+is expecting.
+
+The Rails app generates a unique hash of the asset, which is used as the
+asset's `integrity` attribute. The browser generates the hash of the asset
+on-load and will reject the asset if the hashes do not match.
+
+All CSS and JavaScript assets should use Subresource Integrity. For implementation details,
+see the documentation for [the Sprockets implementation of SRI][sprockets-sri].
+
+Some resources on implementing Subresource Integrity:
+
+- [MDN Article on SRI][mdn-sri]
+- [Subresource Integrity on the GitHub Engineering Blog][github-eng-sri]
+
+-->
+
+### Including external resources
+
+External fonts, CSS, and JavaScript should never be used with the exception of
+Google Analytics and Piwik - and only when the instance has enabled it. Assets
+should always be hosted and served locally from the GitLab instance. Embedded
+resources via `iframes` should never be used except in certain circumstances
+such as with ReCaptcha, which cannot be used without an `iframe`.
+
+### Avoiding inline scripts and styles
+
+In order to protect users from [XSS vulnerabilities][xss], we will disable inline scripts in the future using Content Security Policy.
+
+While inline scripts can be useful, they're also a security concern. If
+user-supplied content is unintentionally left un-sanitized, malicious users can
+inject scripts into the web app.
+
+Inline styles should be avoided in almost all cases, they should only be used
+when no alternatives can be found. This allows reusability of styles as well as
+readability.
+
+## Style guides and linting
+
+See the relevant style guides for our guidelines and for information on linting:
+
+- [SCSS][scss-style-guide]
+
+## Testing
+
+Feature tests need to be written for all new features. Regression tests
+also need to be written for all bug fixes to prevent them from occurring
+again in the future.
+
+See [the Testing Standards and Style Guidelines](testing.md) for more
+information.
+
+## Supported browsers
+
+For our currently-supported browsers, see our [requirements][requirements].
+
+[rails]: http://rubyonrails.org/
+[haml]: http://haml.info/
+[hamlit]: https://github.com/k0kubun/hamlit
+[hamlit-limits]: https://github.com/k0kubun/hamlit/blob/master/REFERENCE.md#limitations
+[scss]: http://sass-lang.com/
+[es6]: https://babeljs.io/
+[sprockets]: https://github.com/rails/sprockets
+[jquery]: https://jquery.com/
+[vue]: http://vuejs.org/
+[vue-docs]: http://vuejs.org/guide/index.html
+[web-page-test]: http://www.webpagetest.org/
+[pagespeed-insights]: https://developers.google.com/speed/pagespeed/insights/
+[google-devtools-profiling]: https://developers.google.com/web/tools/chrome-devtools/profile/?hl=en
+[browser-diet]: https://browserdiet.com/
+[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
+[chrome-accessibility-developer-tools]: https://github.com/GoogleChrome/accessibility-developer-tools
+[audit-rules]: https://github.com/GoogleChrome/accessibility-developer-tools/wiki/Audit-Rules
+[observatory-cli]: https://github.com/mozilla/http-observatory-cli)
+[qualys-ssl]: https://www.ssllabs.com/ssltest/analyze.html
+[secure_headers]: https://github.com/twitter/secureheaders
+[mdn-csp]: https://developer.mozilla.org/en-US/docs/Web/Security/CSP
+[github-eng-csp]: http://githubengineering.com/githubs-csp-journey/
+[dropbox-csp-1]: https://blogs.dropbox.com/tech/2015/09/on-csp-reporting-and-filtering/
+[dropbox-csp-2]: https://blogs.dropbox.com/tech/2015/09/unsafe-inline-and-nonce-deployment/
+[dropbox-csp-3]: https://blogs.dropbox.com/tech/2015/09/csp-the-unexpected-eval/
+[dropbox-csp-4]: https://blogs.dropbox.com/tech/2015/09/csp-third-party-integrations-and-privilege-separation/
+[mdn-sri]: https://developer.mozilla.org/en-US/docs/Web/Security/Subresource_Integrity
+[github-eng-sri]: http://githubengineering.com/subresource-integrity/
+[sprockets-sri]: https://github.com/rails/sprockets-rails#sri-support
+[xss]: https://en.wikipedia.org/wiki/Cross-site_scripting
+[scss-style-guide]: scss_styleguide.md
+[requirements]: ../install/requirements.md#supported-web-browsers
diff --git a/doc/development/licensing.md b/doc/development/licensing.md
index 8c8c7486fff..05972b33fdb 100644
--- a/doc/development/licensing.md
+++ b/doc/development/licensing.md
@@ -54,6 +54,7 @@ Libraries with the following licenses are acceptable for use:
- [BSD 2-Clause License][BSD-2-Clause]: A permissive (non-copyleft) license as defined by the Open Source Initiative.
- [BSD 3-Clause License][BSD-3-Clause] (also known as New BSD or Modified BSD): A permissive (non-copyleft) license as defined by the Open Source Initiative
- [ISC License][ISC] (also known as the OpenBSD License): A permissive (non-copyleft) license as defined by the Open Source Initiative.
+- [Creative Commons Zero (CC0)][CC0]: A public domain dedication, recommended as a way to disclaim copyright on your work to the maximum extent possible.
## Unacceptable Licenses
@@ -85,6 +86,7 @@ Gems which are included only in the "development" or "test" groups by Bundler ar
[BSD-2-Clause]: https://opensource.org/licenses/BSD-2-Clause
[BSD-3-Clause]: https://opensource.org/licenses/BSD-3-Clause
[ISC]: https://opensource.org/licenses/ISC
+[CC0]: https://creativecommons.org/publicdomain/zero/1.0/
[GPL]: http://choosealicense.com/licenses/gpl-3.0/
[GPLv2]: http://www.gnu.org/licenses/gpl-2.0.txt
[GPLv3]: http://www.gnu.org/licenses/gpl-3.0.txt
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 295eae0a88e..61b0fbc89c9 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -9,10 +9,10 @@ a big burden for most organizations. For this reason it is important that your
migrations are written carefully, can be applied online and adhere to the style guide below.
Migrations should not require GitLab installations to be taken offline unless
-_absolutely_ necessary. If a migration requires downtime this should be
-clearly mentioned during the review process as well as being documented in the
-monthly release post. For more information see the "Downtime Tagging" section
-below.
+_absolutely_ necessary - see the ["What Requires Downtime?"](what_requires_downtime.md)
+page. If a migration requires downtime, this should be clearly mentioned during
+the review process, as well as being documented in the monthly release post. For
+more information, see the "Downtime Tagging" section below.
When writing your migrations, also consider that databases might have stale data
or inconsistencies and guard for that. Try to make as little assumptions as possible
diff --git a/doc/gitlab-basics/README.md b/doc/gitlab-basics/README.md
index 3aa83975ace..d7e3aa35bdd 100644
--- a/doc/gitlab-basics/README.md
+++ b/doc/gitlab-basics/README.md
@@ -2,14 +2,14 @@
Step-by-step guides on the basics of working with Git and GitLab.
+- [Command line basics](command-line-commands.md)
- [Start using Git on the command line](start-using-git.md)
- [Create and add your SSH Keys](create-your-ssh-keys.md)
-- [Command Line basics](command-line-commands.md)
- [Create a project](create-project.md)
- [Create a group](create-group.md)
- [Create a branch](create-branch.md)
- [Fork a project](fork-project.md)
- [Add a file](add-file.md)
- [Add an image](add-image.md)
-- [Create a Merge Request](add-merge-request.md)
-- [Create an Issue](create-issue.md)
+- [Create an issue](create-issue.md)
+- [Create a merge request](add-merge-request.md)
diff --git a/doc/gitlab-basics/add-file.md b/doc/gitlab-basics/add-file.md
index ff10a98e8f5..e9fbcbc23a9 100644
--- a/doc/gitlab-basics/add-file.md
+++ b/doc/gitlab-basics/add-file.md
@@ -1,27 +1,5 @@
# How to add a file
-You can create a file in your [shell](command-line-commands.md) or in GitLab.
-
-To create a file in GitLab, sign in to GitLab.
-
-Select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-It's a good idea to [create a branch](create-branch.md), but it's not necessary.
-
-Go to the directory where you'd like to add the file and click on the "+" sign next to the name of the project and directory:
-
-![Create a file](basicsimages/create_file.png)
-
-Name your file (you can't add spaces, so you can use hyphens or underscores). Don't forget to include the markup language you'd like to use :
-
-![File name](basicsimages/file_name.png)
-
-Add all the information that you'd like to include in your file:
-
-![Add information](basicsimages/white_space.png)
-
-Add a commit message based on what you just added and then click on "commit changes":
-
-![Commit changes](basicsimages/commit_changes.png)
+You can create a file in your [terminal](command-line-commands.md) and push
+to GitLab or you can use the
+[web interface](../user/project/repository/web_editor.md#create-a-file).
diff --git a/doc/gitlab-basics/add-merge-request.md b/doc/gitlab-basics/add-merge-request.md
index 236b4248ea2..bf01fe51dc3 100644
--- a/doc/gitlab-basics/add-merge-request.md
+++ b/doc/gitlab-basics/add-merge-request.md
@@ -1,42 +1,33 @@
# How to create a merge request
-Merge Requests are useful to integrate separate changes that you've made to a project, on different branches.
+Merge requests are useful to integrate separate changes that you've made to a
+project, on different branches. This is a brief guide on how to create a merge
+request. For more information, check the
+[merge requests documentation](../user/project/merge_requests.md).
-To create a new Merge Request, sign in to GitLab.
+---
-Go to the project where you'd like to merge your changes:
+1. Before you start, you should have already [created a branch](create-branch.md)
+ and [pushed your changes](basic-git-commands.md) to GitLab.
-![Select a project](basicsimages/select_project.png)
+1. You can then go to the project where you'd like to merge your changes and
+ click on the **Merge requests** tab.
-Click on "Merge Requests" on the left side of your screen:
+ ![Merge requests](img/project_navbar.png)
-![Merge requests](basicsimages/merge_requests.png)
+1. Click on **New merge request** on the right side of the screen.
-Click on "+ new Merge Request" on the right side of the screen:
+ ![New Merge Request](img/merge_request_new.png)
-![New Merge Request](basicsimages/new_merge_request.png)
+1. Select a source branch and click on the **Compare branches and continue** button.
-Select a source branch or branch:
+ ![Select a branch](img/merge_request_select_branch.png)
-![Select a branch](basicsimages/select_branch.png)
+1. At a minimum, add a title and a description to your merge request. Optionally,
+ select a user to review your merge request and to accept or close it. You may
+ also select a milestone and labels.
-Click on the "compare branches" button:
+ ![New merge request page](img/merge_request_page.png)
-![Compare branches](basicsimages/compare_branches.png)
-
-Add a title and a description to your Merge Request:
-
-![Add a title and description](basicsimages/title_description_mr.png)
-
-Select a user to review your Merge Request and to accept or close it. You may also select milestones and labels (they are optional). Then click on the "submit new Merge Request" button:
-
-![Add a new merge request](basicsimages/add_new_merge_request.png)
-
-Your Merge Request will be ready to be approved and published.
-
-### Note
-
-After you created a new branch, you'll immediately find a "create a Merge Request" button at the top of your screen.
-You may automatically create a Merge Request from your recently created branch when clicking on this button:
-
-![Automatic MR button](basicsimages/button-create-mr.png)
+1. When ready, click on the **Submit merge request** button. Your merge request
+ will be ready to be approved and published.
diff --git a/doc/gitlab-basics/basicsimages/add_new_merge_request.png b/doc/gitlab-basics/basicsimages/add_new_merge_request.png
deleted file mode 100644
index e60992c4c6a..00000000000
--- a/doc/gitlab-basics/basicsimages/add_new_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/add_sshkey.png b/doc/gitlab-basics/basicsimages/add_sshkey.png
deleted file mode 100644
index 89c86018629..00000000000
--- a/doc/gitlab-basics/basicsimages/add_sshkey.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_info.png b/doc/gitlab-basics/basicsimages/branch_info.png
deleted file mode 100644
index 2264f3c5bf2..00000000000
--- a/doc/gitlab-basics/basicsimages/branch_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branch_name.png b/doc/gitlab-basics/basicsimages/branch_name.png
deleted file mode 100644
index 75fe8313611..00000000000
--- a/doc/gitlab-basics/basicsimages/branch_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/branches.png b/doc/gitlab-basics/basicsimages/branches.png
deleted file mode 100644
index 8621bc05776..00000000000
--- a/doc/gitlab-basics/basicsimages/branches.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/button-create-mr.png b/doc/gitlab-basics/basicsimages/button-create-mr.png
deleted file mode 100644
index b52ab148839..00000000000
--- a/doc/gitlab-basics/basicsimages/button-create-mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/click-on-new-group.png b/doc/gitlab-basics/basicsimages/click-on-new-group.png
deleted file mode 100644
index 6450deec6fc..00000000000
--- a/doc/gitlab-basics/basicsimages/click-on-new-group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_changes.png b/doc/gitlab-basics/basicsimages/commit_changes.png
deleted file mode 100644
index a88809c5a3f..00000000000
--- a/doc/gitlab-basics/basicsimages/commit_changes.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commit_message.png b/doc/gitlab-basics/basicsimages/commit_message.png
deleted file mode 100644
index 4abe4517f98..00000000000
--- a/doc/gitlab-basics/basicsimages/commit_message.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/commits.png b/doc/gitlab-basics/basicsimages/commits.png
deleted file mode 100644
index 2bfcaf75f01..00000000000
--- a/doc/gitlab-basics/basicsimages/commits.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/compare_branches.png b/doc/gitlab-basics/basicsimages/compare_branches.png
deleted file mode 100644
index 8a18453dd05..00000000000
--- a/doc/gitlab-basics/basicsimages/compare_branches.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_file.png b/doc/gitlab-basics/basicsimages/create_file.png
deleted file mode 100644
index 5ebe1b227dd..00000000000
--- a/doc/gitlab-basics/basicsimages/create_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/create_group.png b/doc/gitlab-basics/basicsimages/create_group.png
deleted file mode 100644
index 7ecc3baa990..00000000000
--- a/doc/gitlab-basics/basicsimages/create_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/edit_file.png b/doc/gitlab-basics/basicsimages/edit_file.png
deleted file mode 100644
index 9d3e817d036..00000000000
--- a/doc/gitlab-basics/basicsimages/edit_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_located.png b/doc/gitlab-basics/basicsimages/file_located.png
deleted file mode 100644
index e357cb5c6ab..00000000000
--- a/doc/gitlab-basics/basicsimages/file_located.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/file_name.png b/doc/gitlab-basics/basicsimages/file_name.png
deleted file mode 100644
index 01639c77d0d..00000000000
--- a/doc/gitlab-basics/basicsimages/file_name.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_file.png b/doc/gitlab-basics/basicsimages/find_file.png
deleted file mode 100644
index 6f26d26ae18..00000000000
--- a/doc/gitlab-basics/basicsimages/find_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/find_group.png b/doc/gitlab-basics/basicsimages/find_group.png
deleted file mode 100644
index 1211510aae9..00000000000
--- a/doc/gitlab-basics/basicsimages/find_group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/fork.png b/doc/gitlab-basics/basicsimages/fork.png
deleted file mode 100644
index 13ff8345616..00000000000
--- a/doc/gitlab-basics/basicsimages/fork.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/group_info.png b/doc/gitlab-basics/basicsimages/group_info.png
deleted file mode 100644
index 2507d6c295b..00000000000
--- a/doc/gitlab-basics/basicsimages/group_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/groups.png b/doc/gitlab-basics/basicsimages/groups.png
deleted file mode 100644
index ef3dca60cc8..00000000000
--- a/doc/gitlab-basics/basicsimages/groups.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/https.png b/doc/gitlab-basics/basicsimages/https.png
deleted file mode 100644
index e74dbc13f9a..00000000000
--- a/doc/gitlab-basics/basicsimages/https.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/image_file.png b/doc/gitlab-basics/basicsimages/image_file.png
deleted file mode 100644
index 7f304b8e1f2..00000000000
--- a/doc/gitlab-basics/basicsimages/image_file.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issue_title.png b/doc/gitlab-basics/basicsimages/issue_title.png
deleted file mode 100644
index 60a6f7973be..00000000000
--- a/doc/gitlab-basics/basicsimages/issue_title.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/issues.png b/doc/gitlab-basics/basicsimages/issues.png
deleted file mode 100644
index 14e9cdb64e1..00000000000
--- a/doc/gitlab-basics/basicsimages/issues.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/key.png b/doc/gitlab-basics/basicsimages/key.png
deleted file mode 100644
index 04400173ce8..00000000000
--- a/doc/gitlab-basics/basicsimages/key.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/merge_requests.png b/doc/gitlab-basics/basicsimages/merge_requests.png
deleted file mode 100644
index 570164df18b..00000000000
--- a/doc/gitlab-basics/basicsimages/merge_requests.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_issue.png b/doc/gitlab-basics/basicsimages/new_issue.png
deleted file mode 100644
index 94e7503dd8b..00000000000
--- a/doc/gitlab-basics/basicsimages/new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_merge_request.png b/doc/gitlab-basics/basicsimages/new_merge_request.png
deleted file mode 100644
index 842f5ebed74..00000000000
--- a/doc/gitlab-basics/basicsimages/new_merge_request.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/new_project.png b/doc/gitlab-basics/basicsimages/new_project.png
deleted file mode 100644
index 421e8bc247b..00000000000
--- a/doc/gitlab-basics/basicsimages/new_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/newbranch.png b/doc/gitlab-basics/basicsimages/newbranch.png
deleted file mode 100644
index d5fcf33c4ea..00000000000
--- a/doc/gitlab-basics/basicsimages/newbranch.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/paste_sshkey.png b/doc/gitlab-basics/basicsimages/paste_sshkey.png
deleted file mode 100644
index 578ebee4440..00000000000
--- a/doc/gitlab-basics/basicsimages/paste_sshkey.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/profile_settings.png b/doc/gitlab-basics/basicsimages/profile_settings.png
deleted file mode 100644
index cb3f79f1879..00000000000
--- a/doc/gitlab-basics/basicsimages/profile_settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/project_info.png b/doc/gitlab-basics/basicsimages/project_info.png
deleted file mode 100644
index e1adb8d48c2..00000000000
--- a/doc/gitlab-basics/basicsimages/project_info.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group.png b/doc/gitlab-basics/basicsimages/select-group.png
deleted file mode 100644
index 33b978dd899..00000000000
--- a/doc/gitlab-basics/basicsimages/select-group.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select-group2.png b/doc/gitlab-basics/basicsimages/select-group2.png
deleted file mode 100644
index aee22c638db..00000000000
--- a/doc/gitlab-basics/basicsimages/select-group2.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_branch.png b/doc/gitlab-basics/basicsimages/select_branch.png
deleted file mode 100644
index f72a3ffb57f..00000000000
--- a/doc/gitlab-basics/basicsimages/select_branch.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/select_project.png b/doc/gitlab-basics/basicsimages/select_project.png
deleted file mode 100644
index 3bb832ea8d0..00000000000
--- a/doc/gitlab-basics/basicsimages/select_project.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/settings.png b/doc/gitlab-basics/basicsimages/settings.png
deleted file mode 100644
index 78637013d9b..00000000000
--- a/doc/gitlab-basics/basicsimages/settings.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/shh_keys.png b/doc/gitlab-basics/basicsimages/shh_keys.png
deleted file mode 100644
index c87f11a9d3d..00000000000
--- a/doc/gitlab-basics/basicsimages/shh_keys.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/submit_new_issue.png b/doc/gitlab-basics/basicsimages/submit_new_issue.png
deleted file mode 100644
index 78b854c8903..00000000000
--- a/doc/gitlab-basics/basicsimages/submit_new_issue.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/title_description_mr.png b/doc/gitlab-basics/basicsimages/title_description_mr.png
deleted file mode 100644
index c31d61ec336..00000000000
--- a/doc/gitlab-basics/basicsimages/title_description_mr.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/white_space.png b/doc/gitlab-basics/basicsimages/white_space.png
deleted file mode 100644
index eaa969bdcf4..00000000000
--- a/doc/gitlab-basics/basicsimages/white_space.png
+++ /dev/null
Binary files differ
diff --git a/doc/gitlab-basics/command-line-commands.md b/doc/gitlab-basics/command-line-commands.md
index addd3b6b6eb..3b075ff5fc0 100644
--- a/doc/gitlab-basics/command-line-commands.md
+++ b/doc/gitlab-basics/command-line-commands.md
@@ -4,18 +4,21 @@
In Git, when you copy a project you say you "clone" it. To work on a git project locally (from your own computer), you will need to clone it. To do this, sign in to GitLab.
-When you are on your Dashboard, click on the project that you'd like to clone, which you'll find at the right side of your screen.
+When you are on your Dashboard, click on the project that you'd like to clone.
+To work in the project, you can copy a link to the Git repository through a SSH
+or a HTTPS protocol. SSH is easier to use after it's been
+[setup](create-your-ssh-keys.md). While you are at the **Project** tab, select
+HTTPS or SSH from the dropdown menu and copy the link using the 'Copy to clipboard'
+button (you'll have to paste it on your shell in the next step).
-![Select a project](basicsimages/select_project.png)
-
-To work in the project, you can copy a link to the Git repository through a SSH or a HTTPS protocol. SSH is easier to use after it's been [setup](create-your-ssh-keys.md). When you're in the project, click on the HTTPS or SSH button at the right side of your screen. Then copy the link (you'll have to paste it on your shell in the next step).
-
-![Copy the HTTPS or SSH](basicsimages/https.png)
+![Copy the HTTPS or SSH](img/project_clone_url.png)
## On the command line
### Clone your project
+
Go to your computer's shell and type the following command:
+
```
git clone PASTE HTTPS OR SSH HERE
```
@@ -23,26 +26,31 @@ git clone PASTE HTTPS OR SSH HERE
A clone of the project will be created in your computer.
### Go into a project, directory or file to work in it
+
```
cd NAME-OF-PROJECT-OR-FILE
```
### Go back one directory or file
+
```
cd ../
```
### View what’s in the directory that you are in
+
```
ls
```
### Create a directory
+
```
mkdir NAME-OF-YOUR-DIRECTORY
```
### Create a README.md or file in directory
+
```
touch README.md
nano README.md
@@ -53,27 +61,33 @@ nano README.md
```
### Remove a file
+
```
rm NAME-OF-FILE
```
### Remove a directory and all of its contents
+
```
rm -rf NAME-OF-DIRECTORY
```
### View history in the command line
+
```
history
```
### Carry out commands for which the account you are using lacks authority
+
You will be asked for an administrator’s password.
+
```
sudo
```
### Tell where you are
+
```
pwd
```
diff --git a/doc/gitlab-basics/create-branch.md b/doc/gitlab-basics/create-branch.md
index 7556b0f663e..ad94f0dad29 100644
--- a/doc/gitlab-basics/create-branch.md
+++ b/doc/gitlab-basics/create-branch.md
@@ -2,38 +2,11 @@
A branch is an independent line of development.
-New commits are recorded in the history for the current branch, which results in taking the source from someone’s repository (the place where the history of your work is stored) at certain point in time, and apply your own changes to it in the history of the project.
-
-To add changes to your GitLab project, you should create a branch. You can do it in your [shell](basic-git-commands.md) or in GitLab.
-
-To create a new branch in GitLab, sign in and then select a project on the right side of your screen:
-
-![Select a project](basicsimages/select_project.png)
-
-Click on "commits" on the menu on the left side of your screen:
-
-![Commits](basicsimages/commits.png)
-
-Click on the "branches" tab:
-
-![Branches](basicsimages/branches.png)
-
-Click on the "new branch" button on the right side of the screen:
-
-![New branch](basicsimages/newbranch.png)
-
-Fill out the information required:
-
-1. Add a name for your new branch (you can't add spaces, so you can use hyphens or underscores)
-
-1. On the "create from" space, add the the name of the branch you want to branch off from
-
-1. Click on the button "create branch"
-
-![Branch info](basicsimages/branch_info.png)
-
-### Note:
-
-You will be able to find and select the name of your branch in the white box next to a project's name:
-
-![Branch name](basicsimages/branch_name.png)
+New commits are recorded in the history for the current branch, which results
+in taking the source from someone’s repository (the place where the history of
+your work is stored) at certain point in time, and apply your own changes to it
+in the history of the project.
+
+To add changes to your GitLab project, you should create a branch. You can do
+it in your [terminal](basic-git-commands.md) or by
+[using the web interface](../user/project/repository/web_editor.md#create-a-new-branch).
diff --git a/doc/gitlab-basics/create-group.md b/doc/gitlab-basics/create-group.md
index f80ae62e442..64274ccd5eb 100644
--- a/doc/gitlab-basics/create-group.md
+++ b/doc/gitlab-basics/create-group.md
@@ -1,43 +1,48 @@
# How to create a group in GitLab
-## Create a group
-
Your projects in GitLab can be organized in 2 different ways:
-under your own namespace for single projects, such as ´your-name/project-1'; or under groups.
-If you organize your projects under a group, it works like a folder. You can manage your group members' permissions and access to the projects.
-
-To create a group, follow the instructions below:
+under your own namespace for single projects, such as `your-name/project-1` or
+under groups.
-Sign in to [GitLab.com](https://gitlab.com).
+If you organize your projects under a group, it works like a folder. You can
+manage your group members' permissions and access to the projects.
-When you are on your Dashboard, click on "Groups" on the left menu of your screen:
+---
-![Go to groups](basicsimages/select-group2.png)
+To create a group:
-Click on "New group" on the top right side of your screen:
+1. Expand the left sidebar by clicking the three bars at the upper left corner
+ and then navigate to **Groups**.
-![New group](basicsimages/click-on-new-group.png)
+ ![Go to groups](img/create_new_group_sidebar.png)
-Fill out the information required:
+1. Once in your groups dashboard, click on **New group**.
-1. Add a group path or group name (you can't add spaces, so you can use hyphens or underscores)
+ ![Create new group information](img/create_new_group_info.png)
-1. Add details or a group description
+1. Fill out the needed information:
-1. You can choose a group avatar if you'd like
+ 1. Set the "Group path" which will be the namespace under which your projects
+ will be hosted (path can contain only letters, digits, underscores, dashes
+ and dots; it cannot start with dashes or end in dot).
+ 1. Optionally, you can add a description so that others can briefly understand
+ what this group is about.
+ 1. Optionally, choose and avatar for your project.
+ 1. Choose the [visibility level](../public_access/public_access.md).
-1. Click on "create group"
+1. Finally, click the **Create group** button.
-![Group information](basicsimages/group_info.png)
-
-## Add a project to a group
+## Add a new project to a group
There are 2 different ways to add a new project to a group:
-* Select a group and then click on "New project" on the right side of your screen. Then you can [create a project](create-project.md)
+- Select a group and then click on the **New project** button.
+
+ ![New project](img/create_new_project_from_group.png)
-![New project](basicsimages/new_project.png)
+ You can then continue on [creating a project](create-project.md).
-* When you are [creating a project](create-project.md), click on "create a group" on the bottom right side of your screen
+- While you are [creating a project](create-project.md), select a group namespace
+ you've already created from the dropdown menu.
-![Create a group](basicsimages/create_group.png)
+ ![Select group](img/select_group_dropdown.png)
diff --git a/doc/gitlab-basics/create-issue.md b/doc/gitlab-basics/create-issue.md
index da9a165b8f5..13e5a738c89 100644
--- a/doc/gitlab-basics/create-issue.md
+++ b/doc/gitlab-basics/create-issue.md
@@ -1,27 +1,30 @@
# How to create an Issue in GitLab
-The Issue Tracker is a good place to add things that need to be improved or solved in a project.
+The issue tracker is a good place to add things that need to be improved or
+solved in a project.
-To create an Issue, sign in to GitLab.
+---
-Go to the project where you'd like to create the Issue:
+1. Go to the project where you'd like to create the issue and navigate to the
+ **Issues** tab on top.
-![Select a project](basicsimages/select_project.png)
+ ![Issues](img/project_navbar.png)
-Click on "Issues" on the left side of your screen:
+1. Click on the **New issue** button on the right side of your screen.
-![Issues](basicsimages/issues.png)
+ ![New issue](img/new_issue_button.png)
-Click on the "+ new issue" button on the right side of your screen:
+1. At the very minimum, add a title and a description to your issue.
+ You may assign it to a user, add a milestone or add labels (all optional).
-![New issue](basicsimages/new_issue.png)
+ ![Issue title and description](img/new_issue_page.png)
-Add a title and a description to your issue:
+1. When ready, click on **Submit issue**.
-![Issue title and description](basicsimages/issue_title.png)
+---
-You may assign the Issue to a user, add a milestone and add labels (they are all optional). Then click on "submit new issue":
-
-![Submit new issue](basicsimages/submit_new_issue.png)
-
-Your Issue will now be added to the Issue Tracker and will be ready to be reviewed. You can comment on it and mention the people involved. You can also link Issues to the Merge Requests where the Issues are solved. To do this, you can use an [Issue closing pattern](../user/project/issues/automatic_issue_closing.md).
+Your Issue will now be added to the issue tracker of the project you opened it
+at and will be ready to be reviewed. You can comment on it and mention the
+people involved. You can also link issues to the merge requests where the issues
+are solved. To do this, you can use an
+[issue closing pattern](../user/project/issues/automatic_issue_closing.md).
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index f737dffc024..3f45a631b3a 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,21 +1,24 @@
# How to create a project in GitLab
-To create a new project, sign in to GitLab.
+There are two ways to create a new project in GitLab.
-Go to your Dashboard and click on "new project" on the right side of your screen.
+1. While in your dashboard, you can create a new project using the **New project**
+ green button or you can use the cross icon in the upper right corner next to
+ your avatar which is always visible.
-![Create a project](basicsimages/new_project.png)
+ ![Create a project](img/create_new_project_button.png)
-Fill out the required information:
+1. From there you can see several options.
-1. Project path or the name of your project (you can't add spaces, so you can use hyphens or underscores)
+ ![Project information](img/create_new_project_info.png)
-1. Your project's description
+1. Fill out the information:
-1. Select a [visibility level](https://gitlab.com/help/public_access/public_access)
+ 1. "Project name" is the name of your project (you can't use spaces, but you
+ can use hyphens or underscores).
+ 1. The "Project description" is optional and will be shown in your project's
+ dashboard so others can briefly understand what your project is about.
+ 1. Select a [visibility level](../public_access/public_access.md).
+ 1. You can also [import your existing projects](../workflow/importing/README.md).
-1. You can also [import your existing projects](http://docs.gitlab.com/ce/workflow/importing/README.html)
-
-1. Click on "create project"
-
-!![Project information](basicsimages/project_info.png)
+1. Finally, click **Create project**.
diff --git a/doc/gitlab-basics/create-your-ssh-keys.md b/doc/gitlab-basics/create-your-ssh-keys.md
index f31c353f2cf..b6ebe374de3 100644
--- a/doc/gitlab-basics/create-your-ssh-keys.md
+++ b/doc/gitlab-basics/create-your-ssh-keys.md
@@ -1,33 +1,37 @@
# How to create your SSH Keys
-You need to connect your computer to your GitLab account through SSH Keys. They are unique for every computer that you link your GitLab account with.
+1. The first thing you need to do is go to your [command line](start-using-git.md)
+ and follow the [instructions](../ssh/README.md) to generate your SSH key pair.
-## Generate your SSH Key
+1. Once you do that, login to GitLab with your credentials.
+1. On the upper right corner, click on your avatar and go to your **Profile settings**.
-Create an account on GitLab. Sign up and check your email for your confirmation link.
+ ![Profile settings dropdown](img/profile_settings.png)
-After you confirm, go to GitLab and sign in to your account.
+1. Navigate to the **SSH keys** tab.
-## Add your SSH Key
+ ![SSH Keys](img/profile_settings_ssh_keys.png)
-On the left side menu, click on "profile settings" and then click on "SSH Keys":
+3. Paste your **public** key that you generated in the first step in the 'Key'
+ box.
-![SSH Keys](basicsimages/shh_keys.png)
+ ![Paste SSH public key](img/profile_settings_ssh_keys_paste_pub.png)
-Then click on the green button "Add SSH Key":
+1. Optionally, give it a descriptive title so that you can recognize it in the
+ event you add multiple keys.
-![Add SSH Key](basicsimages/add_sshkey.png)
+ ![SSH key title](img/profile_settings_ssh_keys_title.png)
-There, you should paste the SSH Key that your command line will generate for you. Below you'll find the steps to generate it:
+1. Finally, click on **Add key** to add it to GitLab. You will be able to see
+ its fingerprint, its title and creation date.
-![Paste SSH Key](basicsimages/paste_sshkey.png)
+ ![SSH key single page](img/profile_settings_ssh_keys_single_key.png)
-## To generate an SSH Key on your command line
-Go to your [command line](start-using-git.md) and follow the [instructions](../ssh/README.md) to generate it.
+>**Note:**
+Once you add a key, you cannot edit it, only remove it. In case the paste
+didn't work, you will have to remove the offending key and re-add it.
-Copy the SSH Key that your command line created and paste it on the "Key" box on the GitLab page. The title will be added automatically.
+---
-![Paste SSH Key](basicsimages/key.png)
-
-Now, you'll be able to use Git over SSH, instead of Git over HTTP.
+Congratulations! You are now ready to use Git over SSH, instead of Git over HTTP!
diff --git a/doc/gitlab-basics/fork-project.md b/doc/gitlab-basics/fork-project.md
index 5f8b81ea919..6c232fe6086 100644
--- a/doc/gitlab-basics/fork-project.md
+++ b/doc/gitlab-basics/fork-project.md
@@ -1,19 +1,20 @@
# How to fork a project
-A fork is a copy of an original repository that you can put somewhere else
-or where you can experiment and apply changes that you can later decide if
+A fork is a copy of an original repository that you can put in another namespace
+where you can experiment and apply changes that you can later decide if
publishing or not, without affecting your original project.
It takes just a few steps to fork a project in GitLab.
-Sign in to GitLab.
+1. Go to a project's dashboard under the **Project** tab and click on the
+ **Fork** button.
-Select a project on the right side of your screen:
+ ![Click on Fork button](img/fork_new.png)
-![Select a project](basicsimages/select_project.png)
+1. You will be asked where to fork the repository. Click on the user or group
+ to where you'd like to add the forked project.
-Click on the "fork" button on the right side of your screen:
+ ![Choose namespace](img/fork_choose_namespace.png)
-![Fork](basicsimages/fork.png)
-
-Click on the user or group to where you'd like to add the forked project.
+1. After a few moments, depending on the repository's size, the forking will
+ complete.
diff --git a/doc/gitlab-basics/img/create_new_group_info.png b/doc/gitlab-basics/img/create_new_group_info.png
new file mode 100644
index 00000000000..c8eddfd1bbb
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_group_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_group_sidebar.png b/doc/gitlab-basics/img/create_new_group_sidebar.png
new file mode 100644
index 00000000000..28017ee02e0
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_group_sidebar.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
new file mode 100644
index 00000000000..e7c794d943f
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_from_group.png b/doc/gitlab-basics/img/create_new_project_from_group.png
new file mode 100644
index 00000000000..6d41d17f9ca
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_from_group.png
Binary files differ
diff --git a/doc/gitlab-basics/img/create_new_project_info.png b/doc/gitlab-basics/img/create_new_project_info.png
new file mode 100644
index 00000000000..16d56f0707f
--- /dev/null
+++ b/doc/gitlab-basics/img/create_new_project_info.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_choose_namespace.png b/doc/gitlab-basics/img/fork_choose_namespace.png
new file mode 100644
index 00000000000..82c9c3bd39e
--- /dev/null
+++ b/doc/gitlab-basics/img/fork_choose_namespace.png
Binary files differ
diff --git a/doc/gitlab-basics/img/fork_new.png b/doc/gitlab-basics/img/fork_new.png
new file mode 100644
index 00000000000..41885223286
--- /dev/null
+++ b/doc/gitlab-basics/img/fork_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_new.png b/doc/gitlab-basics/img/merge_request_new.png
new file mode 100644
index 00000000000..0aba5743f01
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_new.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_page.png b/doc/gitlab-basics/img/merge_request_page.png
new file mode 100644
index 00000000000..68c3bbf9444
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/merge_request_select_branch.png b/doc/gitlab-basics/img/merge_request_select_branch.png
new file mode 100644
index 00000000000..516436ff6cc
--- /dev/null
+++ b/doc/gitlab-basics/img/merge_request_select_branch.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_button.png b/doc/gitlab-basics/img/new_issue_button.png
new file mode 100644
index 00000000000..46b626bed65
--- /dev/null
+++ b/doc/gitlab-basics/img/new_issue_button.png
Binary files differ
diff --git a/doc/gitlab-basics/img/new_issue_page.png b/doc/gitlab-basics/img/new_issue_page.png
new file mode 100644
index 00000000000..843504130b7
--- /dev/null
+++ b/doc/gitlab-basics/img/new_issue_page.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings.png b/doc/gitlab-basics/img/profile_settings.png
new file mode 100644
index 00000000000..f0abd478849
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys.png b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
new file mode 100644
index 00000000000..2c9a42fe10c
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
new file mode 100644
index 00000000000..cd7add6937f
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_paste_pub.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
new file mode 100644
index 00000000000..095beb02be8
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_single_key.png
Binary files differ
diff --git a/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
new file mode 100644
index 00000000000..4b998a7f948
--- /dev/null
+++ b/doc/gitlab-basics/img/profile_settings_ssh_keys_title.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_clone_url.png b/doc/gitlab-basics/img/project_clone_url.png
new file mode 100644
index 00000000000..eed430e1036
--- /dev/null
+++ b/doc/gitlab-basics/img/project_clone_url.png
Binary files differ
diff --git a/doc/gitlab-basics/img/project_navbar.png b/doc/gitlab-basics/img/project_navbar.png
new file mode 100644
index 00000000000..97cf3cd9702
--- /dev/null
+++ b/doc/gitlab-basics/img/project_navbar.png
Binary files differ
diff --git a/doc/gitlab-basics/basicsimages/public_file_link.png b/doc/gitlab-basics/img/public_file_link.png
index f60df6807f4..f60df6807f4 100644
--- a/doc/gitlab-basics/basicsimages/public_file_link.png
+++ b/doc/gitlab-basics/img/public_file_link.png
Binary files differ
diff --git a/doc/gitlab-basics/img/select_group_dropdown.png b/doc/gitlab-basics/img/select_group_dropdown.png
new file mode 100644
index 00000000000..7d8b89c2df9
--- /dev/null
+++ b/doc/gitlab-basics/img/select_group_dropdown.png
Binary files differ
diff --git a/doc/gitlab-basics/start-using-git.md b/doc/gitlab-basics/start-using-git.md
index b61f436c1a4..42cd8bb3e48 100644
--- a/doc/gitlab-basics/start-using-git.md
+++ b/doc/gitlab-basics/start-using-git.md
@@ -1,11 +1,10 @@
# Start using Git on the command line
-If you want to start using a Git and GitLab, make sure that you have created an
-account on GitLab.
+If you want to start using Git and GitLab, make sure that you have created and/or signed into an account on GitLab.
## Open a shell
-Depending on your operating system, find the shell of your preference. Here are some suggestions.
+Depending on your operating system, you will need to use a shell of your preference. Here are some suggestions:
- [Terminal](http://blog.teamtreehouse.com/introduction-to-the-mac-os-x-command-line) on Mac OSX
@@ -22,19 +21,19 @@ Type the following command and then press enter:
git --version
```
-You should receive a message that will tell you which Git version you have in your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
+You should receive a message that will tell you which Git version you have on your computer. If you don’t receive a "Git version" message, it means that you need to [download Git](https://git-scm.com/book/en/v2/Getting-Started-Installing-Git).
If Git doesn't automatically download, there's an option on the website to [download manually](https://git-scm.com/downloads). Then follow the steps on the installation window.
-After you finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
+After you are finished installing, open a new shell and type "git --version" again to verify that it was correctly installed.
## Add your Git username and set your email
-It is important because every Git commit that you create will use this information.
+It is important to configure your Git username and email address as every Git commit will use this information to identify you as the author.
On your shell, type the following command to add your username:
```
-git config --global user.name ADD YOUR USERNAME
+git config --global user.name "YOUR_USERNAME"
```
Then verify that you have the correct username:
@@ -44,7 +43,7 @@ git config --global user.name
To set your email address, type the following command:
```
-git config --global user.email ADD YOUR EMAIL
+git config --global user.email "your_email_address@example.com"
```
To verify that you entered your email correctly, type:
@@ -52,7 +51,7 @@ To verify that you entered your email correctly, type:
git config --global user.email
```
-You'll need to do this only once because you are using the "--global" option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the "--global" option when you’re in that project.
+You'll need to do this only once as you are using the `--global` option. It tells Git to always use this information for anything you do on that system. If you want to override this with a different username or email address for specific projects, you can run the command without the `--global` option when you’re in that project.
## Check your information
@@ -76,7 +75,7 @@ git pull REMOTE NAME-OF-BRANCH -u
(REMOTE: origin) (NAME-OF-BRANCH: could be "master" or an existing branch)
### Create a branch
-Spaces won't be recognized, so you need to use a hyphen or underscore.
+Spaces won't be recognized, so you will need to use a hyphen or underscore.
```
git checkout -b NAME-OF-BRANCH
```
@@ -127,4 +126,3 @@ You need to be in the master branch.
git checkout master
git merge NAME-OF-BRANCH
```
-
diff --git a/doc/install/installation.md b/doc/install/installation.md
index da3f92f9a6c..1fa8678223a 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -108,7 +108,7 @@ Then select 'Internet Site' and press enter to confirm the hostname.
## 2. Ruby
-_**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
+**Note:** The current supported Ruby versions are 2.1.x and 2.3.x. 2.3.x is preferred, and support for 2.1.x will be dropped in the future.
The use of Ruby version managers such as [RVM], [rbenv] or [chruby] with GitLab
in production, frequently leads to hard to diagnose problems. For example,
@@ -268,9 +268,9 @@ sudo usermod -aG redis git
### Clone the Source
# Clone GitLab repository
- sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-12-stable gitlab
+ sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-ce.git -b 8-13-stable gitlab
-**Note:** You can change `8-12-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
+**Note:** You can change `8-13-stable` to `master` if you want the *bleeding edge* version, but never install master on a production server!
### Configure It
@@ -381,7 +381,7 @@ sudo usermod -aG redis git
GitLab Shell is an SSH access and repository management software developed specially for GitLab.
# Run the installation task for gitlab-shell (replace `REDIS_URL` if needed):
- sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production
+ sudo -u git -H bundle exec rake gitlab:shell:install REDIS_URL=unix:/var/run/redis/redis.sock RAILS_ENV=production SKIP_STORAGE_VALIDATION=true
# By default, the gitlab-shell config is generated from your main GitLab config.
# You can review (and modify) the gitlab-shell config as follows:
@@ -400,7 +400,7 @@ If you are not using Linux you may have to run `gmake` instead of
cd /home/git
sudo -u git -H git clone https://gitlab.com/gitlab-org/gitlab-workhorse.git
cd gitlab-workhorse
- sudo -u git -H git checkout v0.8.2
+ sudo -u git -H git checkout v0.8.4
sudo -u git -H make
### Initialize Database and Activate Advanced Features
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 3f4056dc440..26baffdf792 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -2,34 +2,47 @@
![backup banner](backup_hrz.png)
-## Create a backup of the GitLab system
-
-A backup creates an archive file that contains the database, all repositories and all attachments.
-This archive will be saved in backup_path (see `config/gitlab.yml`).
-The filename will be `[TIMESTAMP]_gitlab_backup.tar`. This timestamp can be used to restore an specific backup.
-You can only restore a backup to exactly the same version of GitLab that you created it
-on, for example 7.2.1. The best way to migrate your repositories from one server to
+An application data backup creates an archive file that contains the database,
+all repositories and all attachments.
+This archive will be saved in `backup_path`, which is specified in the
+`config/gitlab.yml` file.
+The filename will be `[TIMESTAMP]_gitlab_backup.tar`, where `TIMESTAMP`
+identifies the time at which each backup was created.
+
+You can only restore a backup to exactly the same version of GitLab on which it
+was created. The best way to migrate your repositories from one server to
another is through backup restore.
-You need to keep separate copies of `/etc/gitlab/gitlab-secrets.json` and
-`/etc/gitlab/gitlab.rb` (for omnibus packages) or
-`/home/git/gitlab/config/secrets.yml` (for installations from source). This file
-contains the database encryption keys used for two-factor authentication and CI
-secret variables, among other things. If you restore a GitLab backup without
-restoring the database encryption key, users who have two-factor authentication
-enabled will lose access to your GitLab server.
+To restore a backup, you will also need to restore `/etc/gitlab/gitlab-secrets.json`
+(for omnibus packages) or `/home/git/gitlab/.secret` (for installations
+from source). This file contains the database encryption key and CI secret
+variables used for two-factor authentication. If you fail to restore this
+encryption key file along with the application data backup, users with two-factor
+authentication enabled will lose access to your GitLab server.
+## Create a backup of the GitLab system
+
+Use this command if you've installed GitLab with the Omnibus package:
```
-# use this command if you've installed GitLab with the Omnibus package
sudo gitlab-rake gitlab:backup:create
-
-# if you've installed GitLab from source
+```
+Use this if you've installed GitLab from source:
+```
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
-Also you can choose what should be backed up by adding environment variable SKIP. Available options: db,
-uploads (attachments), repositories, builds(CI build output logs), artifacts (CI build artifacts), lfs (LFS objects).
-Use a comma to specify several options at the same time.
+You can specify that portions of the application data be skipped using the
+environment variable `SKIP`. You can skip:
+
+- `db` (database)
+- `uploads` (attachments)
+- `repositories` (Git repositories data)
+- `builds` (CI build output logs)
+- `artifacts` (CI build artifacts)
+- `lfs` (LFS objects)
+- `registry` (Container Registry images)
+
+Separate multiple data types to skip using a comma. For example:
```
sudo gitlab-rake gitlab:backup:create SKIP=db,uploads
@@ -69,7 +82,7 @@ Deleting old backups... [SKIPPING]
Starting with GitLab 7.4 you can let the backup script upload the '.tar' file it creates.
It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage.
-But Fog also lets you use [other storage providers](http://fog.io/storage/).
+Fog also supports [other storage providers](http://fog.io/storage/).
For omnibus packages:
@@ -161,7 +174,7 @@ with the name of your bucket:
### Uploading to locally mounted shares
You may also send backups to a mounted share (`NFS` / `CIFS` / `SMB` / etc.) by
-using the [`Local`](https://github.com/fog/fog-local#usage) storage provider.
+using the Fog [`Local`](https://github.com/fog/fog-local#usage) storage provider.
The directory pointed to by the `local_root` key **must** be owned by the `git`
user **when mounted** (mounting with the `uid=` of the `git` user for `CIFS` and
`SMB`) or the user that you are executing the backup tasks under (for omnibus
@@ -228,7 +241,7 @@ of using encryption in the first place!
If you use an Omnibus package please see the [instructions in the readme to backup your configuration](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#backup-and-restore-omnibus-gitlab-configuration).
If you have a cookbook installation there should be a copy of your configuration in Chef.
-If you have an installation from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
+If you installed from source, please consider backing up your `config/secrets.yml` file, `gitlab.yml` file, any SSL keys and certificates, and your [SSH host keys](https://superuser.com/questions/532040/copy-ssh-keys-from-one-server-to-another-server/532079#532079).
At the very **minimum** you should backup `/etc/gitlab/gitlab.rb` and
`/etc/gitlab/gitlab-secrets.json` (Omnibus), or
diff --git a/doc/raketasks/user_management.md b/doc/raketasks/user_management.md
index 8a5e2d6e16b..044b104f5c2 100644
--- a/doc/raketasks/user_management.md
+++ b/doc/raketasks/user_management.md
@@ -70,3 +70,18 @@ sudo gitlab-rake gitlab:two_factor:disable_for_all_users
# installation from source
bundle exec rake gitlab:two_factor:disable_for_all_users RAILS_ENV=production
```
+
+## Clear authentication tokens for all users. Important! Data loss!
+
+Clear authentication tokens for all users in the GitLab database. This
+task is useful if your users' authentication tokens might have been exposed in
+any way. All the existing tokens will become invalid, and new tokens are
+automatically generated upon sign-in or user modification.
+
+```
+# omnibus-gitlab
+sudo gitlab-rake gitlab:users:clear_all_authentication_tokens
+
+# installation from source
+bundle exec rake gitlab:users:clear_all_authentication_tokens RAILS_ENV=production
+```
diff --git a/doc/university/README.md b/doc/university/README.md
new file mode 100644
index 00000000000..8b3538d5616
--- /dev/null
+++ b/doc/university/README.md
@@ -0,0 +1,215 @@
+# GitLab University
+
+GitLab University is the best place to learn about **Version Control with Git and GitLab**.
+
+It doesn't replace, but accompanies our great [Documentation](http://docs.gitlab.com)
+and [Blog Articles](https://about.gitlab.com/blog/).
+
+Would you like to contribute to GitLab University? Then please take a look at our contribution [process](/process) for more information.
+
+## Gitlab University Curriculum
+
+The curriculum is composed of GitLab videos, screencasts, presentations, projects and external GitLab content hosted on other services and has been organized into the following sections.
+
+1. [GitLab Beginner](#beginner)
+1. [GitLab Intermediate](#intermediate)
+1. [GitLab Advanced](#advanced)
+1. [External Articles](#external)
+1. [Resources for GitLab Team Members](#team)
+
+---
+
+### 1. <a name="beginner"></a> GitLab Beginner
+
+#### 1.1. Version Control and Git
+
+1. [Version Control Systems](https://docs.google.com/presentation/d/16sX7hUrCZyOFbpvnrAFrg6tVO5_yT98IgdAqOmXwBho/edit#slide=id.g72f2e4906_2_29)
+1. [Operating Systems and How Git Works](https://drive.google.com/a/gitlab.com/file/d/0B41DBToSSIG_OVYxVFJDOGI3Vzg/view?usp=sharing)
+1. [Code School: An Introduction to Git](https://www.codeschool.com/account/courses/try-git)
+
+#### 1.2. GitLab Basics
+
+1. [An Overview of GitLab.com - Video](https://www.youtube.com/watch?v=WaiL5DGEMR4)
+1. [Why Use Git and GitLab - Slides](https://docs.google.com/a/gitlab.com/presentation/d/1RcZhFmn5VPvoFu6UMxhMOy7lAsToeBZRjLRn0LIdaNc/edit?usp=drive_web)
+1. [GitLab Basics - Article](http://doc.gitlab.com/ce/gitlab-basics/README.html)
+1. [Git and GitLab Basics - Video](https://www.youtube.com/watch?v=03wb9FvO4Ak&index=5&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Git and GitLab Basics - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-23370/material/)
+1. [Comparison of GitLab Versions](https://about.gitlab.com/features/#compare)
+
+#### 1.3. Your GitLab Account
+
+1. [Create a GitLab Account - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/first-steps/create-an-account-on-gitlab/material/)
+1. [Create and Add your SSH key to GitLab - Video](https://www.youtube.com/watch?v=54mxyLo3Mqk)
+
+#### 1.4. GitLab Projects
+
+1. [Repositories, Projects and Groups - Video](https://www.youtube.com/watch?v=4TWfh1aKHHw&index=1&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [Creating a Project in GitLab - Video](https://www.youtube.com/watch?v=7p0hrpNaJ14)
+1. [How to Create Files and Directories](https://about.gitlab.com/2016/02/10/feature-highlight-create-files-and-directories-from-files-page/)
+1. [GitLab Todos](https://about.gitlab.com/2016/03/02/gitlab-todos-feature-highlight/)
+1. [GitLab's Work in Progress (WIP) Flag](https://about.gitlab.com/2016/01/08/feature-highlight-wip/)
+
+#### 1.5. Migrating from other Source Control
+
+1. [Migrating from BitBucket/Stash](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_bitbucket.html)
+1. [Migrating from GitHub](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_github.html)
+1. [Migrating from SVN](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
+1. [Migrating from Fogbugz](http://doc.gitlab.com/ee/workflow/importing/import_projects_from_fogbugz.html)
+
+#### 1.6. GitLab Inc.
+
+1. [About GitLab](https://about.gitlab.com/about/)
+1. [GitLab Direction](https://about.gitlab.com/direction/)
+1. [GitLab Master Plan](https://about.gitlab.com/2016/09/13/gitlab-master-plan/)
+1. [Making GitLab Great for Everyone - Video](https://www.youtube.com/watch?v=GGC40y4vMx0) - Response to "Dear GitHub" letter
+1. [Using Innersourcing to Improve Collaboration](https://about.gitlab.com/2014/09/05/innersourcing-using-the-open-source-workflow-to-improve-collaboration-within-an-organization/)
+1. [The Software Development Market and GitLab - Video](https://www.youtube.com/watch?v=sXlhgPK1NTY&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=6) - [Slides](https://docs.google.com/presentation/d/1vCU-NbZWz8NTNK8Vu3y4zGMAHb5DpC8PE5mHtw1PWfI/edit)
+
+#### 1.7 Community and Support
+
+1. [Getting Help](/getting-help/)
+ - Proposing Features and Reporting and Tracking bugs for GitLab
+ - The GitLab IRC channel, Gitter Chat Room, Community Forum and Mailing List
+ - Getting Technical Support
+ - Being part of our Great Community and Contributing to GitLab
+1. [Getting Started with the GitLab Development Kit (GDK)](https://about.gitlab.com/2016/06/08/getting-started-with-gitlab-development-kit/)
+1. [Contributing Technical Articles to the GitLab Blog](https://about.gitlab.com/2016/01/26/call-for-writers/)
+1. [GitLab Training Workshops](/training)
+
+#### 1.8 GitLab Training Material
+
+1. [Git and GitLab Terminology](/glossary/)
+1. [Git and GitLab Workshop - Slides](https://docs.google.com/presentation/d/1JzTYD8ij9slejV2-TO-NzjCvlvj6mVn9BORePXNJoMI/edit?usp=drive_web)
+1. [Git and GitLab Revision](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/university/training/end-user)
+
+---
+
+### 2. <a name="intermediate"></a> GitLab Intermediate
+
+#### 2.1 GitLab Pages
+
+1. [Using any Static Site Generator with GitLab Pages](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/)
+1. [Securing GitLab Pages with SSL](https://about.gitlab.com/2016/06/24/secure-gitlab-pages-with-startssl/)
+1. [GitLab Pages Documentation](http://doc.gitlab.com/ee/pages/README.html)
+
+#### 2.2. GitLab Issues
+
+1. [Markdown in GitLab](http://doc.gitlab.com/ce/markdown/markdown.html)
+1. [Issues and Merge Requests - Video](https://www.youtube.com/watch?v=raXvuwet78M)
+1. [Due Dates and Milestones fro GitLab Issues](https://about.gitlab.com/2016/08/05/feature-highlight-set-dates-for-issues/)
+1. [How to Use GitLab Labels](https://about.gitlab.com/2016/08/17/using-gitlab-labels/)
+1. [Applying GitLab Labels Automatically](https://about.gitlab.com/2016/08/19/applying-gitlab-labels-automatically/)
+1. [GitLab Issue Board - Product Page](https://about.gitlab.com/solutions/issueboard/)
+1. [An Overview of GitLab Issue Board](https://about.gitlab.com/2016/08/22/announcing-the-gitlab-issue-board/)
+1. [Designing GitLab Issue Board](https://about.gitlab.com/2016/08/31/designing-issue-boards/)
+1. [From Idea to Production with GitLab - Video](https://www.youtube.com/watch?v=25pHyknRgEo&index=14&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+
+#### 2.3. Continuous Integration
+
+1. [Operating Systems, Servers, VMs, Containers and Unix - Video](https://www.youtube.com/watch?v=V61kL6IC-zY&index=8&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [GitLab CI - Product Page](https://about.gitlab.com/gitlab-ci/)
+1. [Getting started with GitLab and GitLab CI](https://about.gitlab.com/2015/12/14/getting-started-with-gitlab-and-gitlab-ci/)
+1. [GitLab Container Registry](https://about.gitlab.com/2016/05/23/gitlab-container-registry/)
+1. [GitLab and Docker - Video](https://www.youtube.com/watch?v=ugOrCcbdHko&index=12&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [How we scale GitLab with built in Docker](https://about.gitlab.com/2016/06/21/how-we-scale-gitlab-by-having-docker-built-in/)
+1. [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+1. [Deployments and Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+1. [Sequential, Parallel or Custom Pipelines](https://about.gitlab.com/2016/07/29/the-basics-of-gitlab-ci/)
+1. [Setting up GitLab Runner For Continuous Integration](https://about.gitlab.com/2016/03/01/gitlab-runner-with-docker/)
+1. [Setting up GitLab Runner on DigitalOcean](https://about.gitlab.com/2016/04/19/how-to-set-up-gitlab-runner-on-digitalocean/)
+1. [Setting up GitLab CI for iOS projects](https://about.gitlab.com/2016/03/10/setting-up-gitlab-ci-for-ios-projects/)
+1. [IBM: Continuous Delivery vs Continuous Deployment - Video](https://www.youtube.com/watch?v=igwFj8PPSnw)
+1. [Amazon: Transition to Continuous Delivery - Video](https://www.youtube.com/watch?v=esEFaY0FDKc)
+1. See **[Integrations](#integrations)** for integrations with other CI services.
+
+#### 2.4. Workflow
+
+1. [GitLab Flow - Video](https://youtu.be/enMumwvLAug?list=PLFGfElNsQthZnwMUFi6rqkyUZkI00OxIV)
+1. [GitLab Flow vs Forking in GitLab - Video](https://www.youtube.com/watch?v=UGotqAUACZA)
+1. [GitLab Flow Overview](https://about.gitlab.com/2014/09/29/gitlab-flow/)
+1. [Always Start with an Issue](https://about.gitlab.com/2016/03/03/start-with-an-issue/)
+1. [GitLab Flow Documentation](http://doc.gitlab.com/ee/workflow/gitlab_flow.html)
+
+#### 2.5. GitLab Comparisons
+
+1. [GitLab Compared to Other Tools](https://about.gitlab.com/comparison/)
+1. [Comparing GitLab Terminology](https://about.gitlab.com/2016/01/27/comparing-terms-gitlab-github-bitbucket/)
+1. [GitLab Compared to Atlassian (Recording 2016-03-03) ](https://youtu.be/Nbzp1t45ERo)
+1. [GitLab Position FAQ](https://about.gitlab.com/handbook/positioning-faq)
+1. [Customer review of GitLab with points on why they prefer GitLab](https://www.enovate.co.uk/web-design-blog/2015/11/25/gitlab-review/)
+
+---
+
+### 3. <a name="advanced"></a> GitLab Advanced
+
+#### 3.1. Dev Ops
+
+1. [Xebia Labs: Dev Ops Terminology](https://xebialabs.com/glossary/)
+1. [Xebia Labs: Periodic Table of DevOps Tools](https://xebialabs.com/periodic-table-of-devops-tools/)
+1. [Puppet Labs: State of Dev Ops 2015 - Book](https://puppetlabs.com/sites/default/files/2015-state-of-devops-report.pdf)
+
+#### 3.2. Installing GitLab with Omnibus
+
+1. [What is Omnibus - Video](https://www.youtube.com/watch?v=XTmpKudd-Oo)
+1. [How to Install GitLab with Omnibus - Video](https://www.youtube.com/watch?v=Q69YaOjqNhg)
+1. [Installing GitLab - Online Course](https://courses.platzi.com/classes/git-gitlab/concepto/part-1/part-3/material/)
+1. [Using a Non-Packaged PostgreSQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-non-packaged-postgresql-database-management-server)
+1. [Using a MySQL Database](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/README.md#using-a-mysql-database-management-server-enterprise-edition-only)
+1. [Installing GitLab on Microsoft Azure](https://about.gitlab.com/2016/07/13/how-to-setup-a-gitlab-instance-on-microsoft-azure/)
+1. [Installing GitLab on Digital Ocean](https://about.gitlab.com/2016/04/27/getting-started-with-gitlab-and-digitalocean/)
+
+#### 3.3. Permissions
+
+1. [How to Manage Permissions in GitLab EE - Video](https://www.youtube.com/watch?v=DjUoIrkiNuM)
+
+#### 3.4. Large Files
+
+1. [Big files in Git (Git LFS, Annex) - Video](https://www.youtube.com/watch?v=DawznUxYDe4)
+
+#### 3.5. LDAP and Active Directory
+
+1. [How to Manage LDAP, Active Directory in GitLab - Video](https://www.youtube.com/watch?v=HPMjM-14qa8)
+
+#### 3.6 Custom Languages
+
+1. [How to add Syntax Highlighting Support for Custom Langauges to GitLab - Video](how to add support for your favorite language to GitLab)
+
+#### 3.7. Scalability and High Availability
+
+1. [Scalability and High Availability - Video](https://www.youtube.com/watch?v=cXRMJJb6sp4&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e&index=2)
+1. [High Availability - Video](https://www.youtube.com/watch?v=36KS808u6bE&index=15&list=PLFGfElNsQthbQu_IWlNOxul0TbS_2JH-e)
+1. [High Availability Documentation](https://about.gitlab.com/high-availability/)
+
+#### 3.8 Cycle Analytics
+
+1. [GitLab Cycle Analytics Overview](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+1. [GitLab Cycle Analytics - Product Page](https://about.gitlab.com/solutions/cycle-analytics/)
+
+#### 3.9. <a name="integrations"></a> Integrations
+
+1. [How to Integrate JIRA and Jenkins with GitLab - Video](https://gitlabmeetings.webex.com/gitlabmeetings/ldr.php?RCID=44b548147a67ab4d8a62274047146415)
+1. [How to Integrate Jira with GitLab](http://doc.gitlab.com/ee/integration/jira.html)
+1. [How to Integrate Jenkins with GitLab](http://doc.gitlab.com/ee/integration/jenkins.html)
+1. [How to Integrate Bamboo with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/project_services/bamboo.md)
+1. [How to Integrate Slack with GitLab](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/integration/slack.md)
+1. [How to Integrate Convox with GitLab](https://about.gitlab.com/2016/06/09/continuous-delivery-with-gitlab-and-convox/)
+1. [Getting Started with GitLab and Shippable CI](https://about.gitlab.com/2016/05/05/getting-started-gitlab-and-shippable/)
+
+---
+
+## 4. <a name="external"></a> External Articles
+
+1. [2011 WSJ article by Mark Andreeson - Software is Eating the World](http://www.wsj.com/articles/SB10001424053111903480904576512250915629460)
+1. [2014 Blog post by Chris Dixon - Software eats software development](http://cdixon.org/2014/04/13/software-eats-software-development/)
+1. [2015 Venture Beat article - Actually, Open Source is Eating the World](http://venturebeat.com/2015/12/06/its-actually-open-source-software-thats-eating-the-world/)
+
+---
+
+## 5. <a name="team"></a> Resources for GitLab Team Members
+
+*Some content can only be accessed by GitLab team members*
+
+1. [Support Path](/support/)
+1. [Sales Path (redirect to sales handbook)](https://about.gitlab.com/handbook/sales-onboarding/)
+1. [GitLab architecture for noobs](https://dev.gitlab.org/gitlab/gitlabhq/blob/master/doc/development/architecture.md)
+1. [Client Assessment of GitLab versus GitHub](https://docs.google.com/a/gitlab.com/spreadsheets/d/18cRF9Y5I6I7Z_ab6qhBEW55YpEMyU4PitZYjomVHM-M/edit?usp=sharing)
diff --git a/doc/university/glossary/README.md b/doc/university/glossary/README.md
new file mode 100644
index 00000000000..a86ff165f2e
--- /dev/null
+++ b/doc/university/glossary/README.md
@@ -0,0 +1,482 @@
+
+## What is the Glossary
+
+This contains a simplified list and definitions of some of the terms that you will encounter in your day to day activities when working with GitLab.
+Please add any terms that you discover that you think would be useful for others.
+
+### 2FA
+
+User authentication by combination of 2 different steps during login. This allows for more security.
+
+### Access Levels
+
+Process of selective restriction to create, view, modify or delete a resource based on a set of assigned permissions.
+See, [GitLab's Permission Guidelines](http://doc.gitlab.com/ce/permissions/permissions.html)
+
+### Active Directory (AD)
+
+A Microsoft based directory service for windows domain networks. It uses LDAP technology under the hood
+
+### Agile
+
+Building and delivering software in phases/parts rather than trying to build everything at once then delivering to the user/client. The later is known as a WaterFall model
+
+### Application Lifecycle Management (ALM)
+
+Entire product lifecycle management process for an application. From requirements management, development and testing until deployment.
+
+### Artifactory
+
+Version control for binaries.
+
+### Artifacts
+
+objects (usually binary and large) created by a build process
+
+### Atlassian
+
+A company that develops software products for developers and project managers including Bitbucket, Jira, Hipchat, Confluence, Bamboo. See [Atlassian] (https://www.atlassian.com)
+
+### Audit Log
+
+*** Needs definition here
+
+### Auto Defined User Group
+
+User groups are a way of centralizing control over important management tasks, particularly access control and password policies.
+A simple example of such groups are the users and the admins groups.
+In most of the cases these groups are auto defined in terms of access, rules of usage, conditions to be part of, etc...
+
+### Bamboo
+
+Atlassian's CI tool similar to GitLab CI and Jenkins
+
+### Basic Subscription
+
+Entry level subscription for GitLab EE currently available in packs of 10 see [Basic subscription](https://about.gitlab.com/pricing/)
+
+### Bitbucket
+
+Atlassian's web hosting service for Git and Mercurial Projects i.e. GitLab.com competitor
+
+### Branch
+
+A branch is a parallel version of a repository. Allows you to work on the repository without you affecting the "master" branch. Allows you to make changes without affecting the current "live" version. When you have made all your changes to your branch you can then merge to the master and to make the changes fo "live".
+
+### Branded Login
+
+Having your own logo on your GitLab instance login page instead of the GitLab logo.
+
+### CEPH
+
+is a distributed object store and file system designed to provide excellent performance, reliability and scalability.
+
+### Clone
+
+A copy of a repository stored on your machine that allows you to use your own editor without being online, but still tracks the changes made remotely.
+
+### Code Review
+
+Examination of a progam's code. The main aim is to maintain high standards quality of code that is being shipped.
+
+### Code Snippet
+
+A small amount of code. Usually for the purpose of showing other developers how
+to do something specific or reproduce a problem.
+
+### Collaborator
+
+Person with read and write access to a repository who has been invited by repository owner.
+
+### Commit
+
+Is a change (revision) to a file, and also creates an ID that allows you to see revision history and who made the changes.
+
+### Community
+
+Everyone who is using GitLab
+
+### Confluence
+
+Atlassian's product for collaboration of documents and projects.
+
+### Continuous Deivery
+
+Continuous delivery is a series of practices designed to ensure that code can be rapidly and safely deployed to production by delivering every change to a production-like environment and ensuring business applications and services function as expected through rigorous automated testing.
+
+### Continuous Deployment
+
+Continuous deployment is the next step of continuous delivery: Every change that passes the automated tests is deployed to production automatically.
+
+### Continuous Integration
+
+A process that involves adding new code commits to source code with the combined code being run on an automated test to ensure that the changes do not break the software.
+
+### Contributor
+
+Term used to a person contributing to an Open Source Project.
+
+### Data Centre
+
+Atlassian product for High Availability.
+
+### Deploy Keys
+
+An SSH key stored on the your server that grants access to a single GitLab repository. This is used by a GitLab runner to clone a project's code so that tests can be run against the checked out code.
+
+### Developer
+
+For us (GitLab) this means a software developer, i.e. someone who makes software. It is also one of the levels of access in our multi level approval system.
+
+### Diff
+
+Is the difference between two commits, or saved changes. This will also be shown visually after the changes.
+
+### Docker
+
+Containers wrap up a piece of software in a complete filesystem that contains everything it needs to run: code, runtime, system tools, system libraries – anything you can install on a server.
+This guarantees that it will always run the same, regardless of the environment it is running in.
+
+### Fork
+
+Your own copy of a repository that allows you to make changes to the repository without affecting the original.
+
+### Gerrit
+
+A code review tool built on top of Git.
+
+### Git Hooks
+
+Are scripts you can use to trigger actions at certain points.
+
+### GitHost.io
+
+Is a single-tenant solution that provides GitLab CE or EE as a managed service. GitLab Inc. is responsible for
+installing, updating, hosting, and backing up customers own private and secure GitLab instance.
+
+### GitHub
+
+A web-based Git repository hosting service with an enterprise offering. Its main features are: issue tracking, pull request with code review, abundancy of integrations and wiki. As of April 2016, the service has over 14 million users. It offers free public repos, private repos and enterprise services are paid.
+
+### GitLab CE
+
+Our free on Premise solution with >100,000 users
+
+### GitLab CI
+
+Our own Continuos Integration feature that is shipped with each instance
+
+### GitLab EE
+
+Our premium on premise solution that currently has Basic, Standard and Plus subscription packages with additional features and support.
+
+### GitLab.com
+
+Our free SaaS for public and private repositories.
+
+### Gitolite
+
+Is basically an access layer that sits on top of Git. Users are granted access to repos via a simple config file and you as an admin only needs the users public SSH key and a username from the user.
+
+### Gitorious
+
+A web based hosting service for projects using Git. It was acquired by GitLab and we discontinued the service. [Gitorious Acquisition Blog Post](https://about.gitlab.com/2015/03/03/gitlab-acquires-gitorious/)
+
+### HADR
+
+Sometimes written HA/DR. High Availability for Disaster Recovery. Usually refers to a strategy having a failover server in place in case the main server fails.
+
+### Hip Chat
+
+Atlassian's real time chat application for teams. Competitor to Slack, RocketChat and MatterMost.
+
+### High Availability
+
+Refers to a system or component that is continuously operational for a desirably long length of time. Availability can be measured relative to "100% operational" or "never failing."
+
+### Issue Tracker
+
+A tool used to manage, organize, and maintain a list of issues, making it easier for an organization to manage.
+
+### Jenkins
+
+An Open Source CI tool written using the Java programming language. Does the same job as GitLab CI, Bamboo, Travis CI. It is extremely popular. see [Jenkins](https://jenkins-ci.org/)
+
+### Jira
+
+Atlassian's project management software. i.e. a complex issue tracker. See[Jira](https://www.atlassian.com/software/jira)
+
+### Kerberos
+
+A network authentication protocol that uses secret-key cryptography for security.
+
+### Kubernetes
+
+An open source container cluster manager originally designed by Google. It's basically a platform for automating deployment, scaling, and operations of application containers over clusters of hosts.
+
+### Labels
+
+An identifier to describe a group of one or more specific file revisions
+
+### LDAP
+
+Lightweight Directory Access Protocol - basically its a directory (electronic address book) with user information e.g. name, phone_number etc
+
+### LDAP User Authentication
+
+Allowing GitLab to sign in people from an LDAP server i.e. Allow people whose names are on the electronic user directory server) to be able to use their LDAP accounts to login.
+
+### LDAP Group Sync
+
+Allows you to synchronize the members of a GitLab group with one or more LDAP groups.
+
+### Git LFS
+
+Git Large File Storage. A way to enable git to handle large binary files by using reference pointers within small text files to point to the large files.
+
+### Linux
+
+An operating system like Windows or OS X. It is mostly used by software developers and on servers.
+
+### Markdown
+
+Is a lightweight markup language with plain text formatting syntax designed so that it can be converted to HTML and many other formats using a tool by the same name.
+Markdown is often used to format readme files, for writing messages in online discussion forums, and to create rich text using a plain text editor.
+
+### Maria DB
+
+A community developed fork/variation of MySQL. MySQL is owned by Oracle.
+
+### Master
+
+Name of the default branch in every git repository.
+
+### Mercurial
+
+A free distributed version control system like Git. Think of it as a competitor to Git.
+
+### Merge
+
+Takes changes from one branch, and applies them into another branch.
+
+### Meteor
+
+A hip platform for building javascript apps.[Meteor] (https://www.meteor.com)
+
+### Milestones
+
+Allows you to track the progress on issues, and merge requests, which allows you to get a snapshot of the progress made.
+
+### Mirror Repositories
+
+You can set up a project to automatically have its branches, tags, and commits updated from an upstream repository. This is useful when a repository you're interested in is located on a different server, and you want to be able to browse its content and its activity using the familiar GitLab interface.
+
+### MIT License
+
+A type of software license. It lets people do anything with your code with proper attribution and without warranty. It is the most common license for open source applications written in Ruby on Rails. GitLab CE is issued under this license.
+This means, you can download the code, modify it as you want even build a new commercial product using the underlying code and its not illegal. The only condition is that there is no form of waranty provided by GitLab so whatever happens if you use the code is your own problem.
+
+### Mondo
+
+*** Needs definition here
+
+### Multi LDAP Server
+
+*** Needs definition here
+
+### My SQL
+
+A relational database. Currently only supported if you are using EE. It is owned by Oracle.
+
+### Namespace
+
+In computing, a namespace is a set of symbols that are used to organize objects of various kinds, so that these objects may be referred to by name.
+
+Prominent examples include:
+- file systems are namespaces that assign names to files;
+- programming languages organize their variables and subroutines in namespaces;
+- computer networks and distributed systems assign names to resources, such as computers, printers, websites, (remote) files, etc.
+
+### Nginx
+
+(pronounced "engine x") is a web server. It can act as a reverse proxy server for HTTP, HTTPS, SMTP, POP3, and IMAP protocols, as well as a load balancer and an HTTP cache.
+
+### oAuth
+
+Is an open standard for authorization, commonly used as a way for Internet users to log into third party websites using their Microsoft, Google, Facebook or Twitter accounts without exposing their password.
+
+### Omnibus Packages
+
+Omnibus is a way to package the different services and tools required to run GitLab, so that users can install it without as much work.
+
+### On Premise
+
+On your own server. In GitLab, this refers to the ability to download GitLab EE/GitLab CE and host it on your own server rather than using GitLab.com which is hosted by GitLab Inc's servers.
+
+### Open Source Software
+
+Software for which the original source code is freely available and may be redistributed and modified.
+
+### Owner
+
+This is the most powerful person on a GitLab project. He has the permissions of all the other users plus the additional permission of being able to destroy i.e. delete the project
+
+### PaaS
+
+Typically referred to in regards to application development, it is a model in which a cloud provider delivers hardware and software tools to its users as a service
+
+### Perforce
+
+The company that produces Helix. A commercial, proprietary, centralised VCS well known for it's ability to version files of any size and type. They OEM a re-branded version of GitLab called "GitSwarm" that is tightly integrated with their "GitFusion" product, which in turn represents a portion of a Helix repository (called a depot) as a git repo
+
+### Phabricator
+
+Is a suite of web-based software development collaboration tools, including the Differential code review tool, the Diffusion repository browser, the Herald change monitoring tool, the Maniphest bug tracker and the Phriction wiki. Phabricator integrates with Git, Mercurial, and Subversion.
+
+### Piwik Analytics
+
+An open source analytics software to help you analyze web traffic. It is similar to google analytics only that google analytics is not open source and information is stored by google while in Piwik the information is stored in your own server hence fully private.
+
+### Plus Subscription
+
+GitLab Premium EE subscription that includes training and dedicated Account Management and Service Engineer and complete support package [Plus subscription](https://about.gitlab.com/pricing/)
+
+### PostgreSQL
+
+A relational database. Touted as the most advanced open source database.
+
+### Protected Branches
+
+A feature that protects branches from unauthorized pushes, force pushing or deletion.
+
+### Pull
+
+Git command to synchronize the local repository with the remote repository, by fetching all remote changes and merging them into the local repository.
+
+### Puppet
+
+A popular devops automation tool
+
+### Push
+
+Git command to send commits from the local repository to the remote repository.
+
+### RE Read Only
+
+Permissions to see a file and it's contents, but not change it
+
+### Rebase
+
+Moves a branch from one commit to another. This allows you to re-write your project's history.
+
+### Git Repository
+
+Storage location of all files which are tracked by git.
+
+### Requirements management
+
+*** Needs definition here
+
+### Revision
+
+*** Needs definition here
+
+### Revision Control
+
+Also known as version control or source control, is the management of changes to documents, computer programs, large web sites, and other collections of information. Changes are usually identified by a number or letter code, termed the "revision number", "revision level", or simply "revision".
+
+### RocketChat
+
+An open source chat application for teams. Very similar to Slack only that is is open-source.
+
+### Runners
+
+Actual build machines/containers that run/execute tests you have specified to be run on GitLab CI
+
+### SaaS
+
+Software as a service. Software is hosted centrally and accessed on-demand i.e. when you want to. This refers to GitLab.com in our scenario
+
+### SCM
+
+Software Configuration Management. Often used by people when they mean Version Control
+
+## Scrum
+
+An Agile framework designed to help complete complex (typically) software projects. It's made up of several parts: product requirments backlog, sprint plannnig, sprint (development), sprint review, retrospec (analyzing the sprint). The goal is to end up with potentially shippable products.
+
+### Scrum Board
+
+The board used to track the status and progress of each of the sprint backlog items.
+
+### Slack
+
+Real time messaging app for teams. Used internally by GitLab
+
+### Slave Servers
+
+Also known as secondary servers. They help to spread the load over multiple machines, they also provide backups when the master/primary server crashes.
+
+### Source Code
+
+Program code as typed by a computer programmer. i.e. it has not yet been compiled/translated by the computer to machine language.
+
+### SSH Key
+
+A unique identifier of a computer. It is used to identify computers without the need for a password. e.g. On GitLab I have added the ssh key of all my work machines so that the GitLab instance knows that it can accept code pushes and pulls from this trusted machines whose keys are I have added.
+
+### SSO
+
+Single Sign On. An authentication process that allows you enter one username and password to access multiple applications.
+
+### Standard Subscription
+
+Our mid range EE subscription that includes 24/7 support, support for High Availability [Standard Subscription](https://about.gitlab.com/pricing/)
+
+### Stash
+
+Atlassian's Git On-Premises solution. Think of it as Atlassian's GitLab EE. It is now known as BitBucket Server.
+
+### Subversion
+
+Non-proprietary, centralized version control system.
+
+### Sudo
+
+A program that allows you to perform superuser/administrator actions on Unix Operating Systems e.g. Linux, OS X. It actually stands for 'superuser do'
+
+### SVN
+
+Abbreviation for Subversion.
+
+### Tag
+
+Represents a version of a particular branch at a moment in time.
+
+### Tool Stack
+
+Set of tools used in a process to achieve a common outcome. E.g. set of tools used in Application Lifecycle Management.
+
+### Trac
+
+An Open Source project management and bug tracking web application.
+
+### User
+
+Anyone interacting with the software.
+
+### VCS
+
+Version Control Software
+
+### Waterfall
+
+A model of building software that involves collecting all requirements from the customer, then building and refining all the requirements and finally delivering the COMPLETE software to the customer that meets all the requirements specified by the customer
+
+### Webhooks
+
+A way for for an app to provide other applications with real-time information. e.g. send a message to a slack channel when a commit is pushed
+
+### Wiki
+
+A website/system that allows for collaborative editing of its content by the users. In programming, they usually contain documentation of how to use the software
diff --git a/doc/university/high-availability/aws/README.md b/doc/university/high-availability/aws/README.md
new file mode 100644
index 00000000000..088f1cd7290
--- /dev/null
+++ b/doc/university/high-availability/aws/README.md
@@ -0,0 +1,387 @@
+
+# High Availability on AWS
+
+GitLab on AWS can leverage many of the services that are already
+configurable with High Availability. These services have a lot of
+flexibility and are able to adopt to most companies, best of all is the
+ability to automate both vertical and horizontal scaling.
+
+In this article we'll go through a basic HA setup where we'll start by
+configuring our Virtual Private Cloud and subnets to later integrate
+services such as RDS for our database server and ElastiCache as a Redis
+cluster to finally manage them within an auto scaling group with custom
+scaling policies.
+
+***
+
+## Where to Start
+
+Login to your AWS account through the `My Account` dropdown on
+`https://aws.amazon.com` or through the URI assigned to your team such as
+`https://myteam.signin.aws.amazon.com/console/`. You'll start on the
+Amazon Web Services console from where we can choose all of the services
+we'll be using to configure our cloud infrastructure.
+
+***
+
+## Network
+
+We'll start by creating a VPC for our GitLab cloud infrastructure, then
+we can create subnets to have public and private instances in at least
+two AZs. Public subnets will require a Route Table keep an associated
+Internet Gateway.
+
+### VPC
+
+Start by looking for the VPC option on the web console. Now create a new
+VPC. We can use `10.0.0.0/16` for the CIDR block and leave tenancy as
+default if we don't require dedicated hardware.
+
+![New VPC](img/new_vpc.png)
+
+If you're setting up the Elastic File System service then select the VPC
+and from the Actions dropdown choose Edit DNS Hostnames and select Yes.
+
+### Subnet
+
+Now let's create some subnets in different Availability Zones. Make sure
+that each subnet is associated the the VPC we just created, that it has
+a distinct VPC and lastly that CIDR blocks don't overlap. This will also
+allow us to enable multi AZ for redundancy.
+
+We will create private and public subnets to match load balancers and
+RDS instances as well.
+
+![Subnet Creation](img/subnet.png)
+
+The subnets are listed with their name, AZ and CIDR block:
+
+* gitlab-public-10.0.0.0 - us-west-2a - 10.0.0.0
+* gitlab-private-10.0.1.0 - us-west-2a - 10.0.1.0
+* gitlab-public-10.0.2.0 - us-west-2b - 10.0.2.0
+* gitlab-private-10.0.3.0 - us-west-2b - 10.0.3.0
+
+### Route Table
+
+Up to now all our subnets are private. We need to create a Route Table
+to associate an Internet Gateway. On the same VPC dashboard choose
+Route Tables on the left column and give it a name and associate it to
+our newly created VPC.
+
+![Route Table](img/route_table.png)
+
+
+### Internet Gateway
+
+Now still on the same dashboard head over to Internet Gateways and
+create a new one. After its created pres on the `Attach to VPC` button and
+select our VPC.
+
+![Internet Gateway](img/ig.png)
+
+### Configure Subnets
+
+Go back to the Router Tables screen and select the newly created one,
+press the Routes tab on the bottom section and edit it. We need to add a
+new target which will be our Internet Gateway and have it receive
+traffic from any destination.
+
+![Subnet Config](img/ig-rt.png)
+
+Before leaving this screen select the next tab to the rgiht which is
+Subnet Associations and add our public subnets. If you followed our
+naming convention they should be easy to find.
+
+***
+
+## Database with RDS
+
+For our database server we will use Amazon RDS which offers Multi AZ
+for redundancy. Lets start by creating a subnet group and then we'll
+create the actual RDS instance.
+
+### Subnet Group
+
+From the RDS dashboard select Subnet Groups. Lets select our VPC from
+the VPC ID dropdown and at the bottom we can add our private subnets.
+
+![Subnet Group](img/db-subnet-group.png)
+
+### RDS
+
+Select the RDS service from the Database section and create a new
+PostgreSQL instance. After choosing between a Production or
+Development instance we'll start with the actual configuration. On the
+image bellow we have the settings for this article but note the
+following two options which are of particular interest for HA:
+
+1. Multi-AZ-Deployment is recommended as redundancy. Read more at
+[High Availability (Multi-AZ)](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/Concepts.MultiAZ.html)
+1. While we chose a General Purpose (SSD) for this article a Provisioned
+IOPS (SSD) is best suited for HA. Read more about it at
+[Storage for Amazon RDS](http://docs.aws.amazon.com/AmazonRDS/latest/UserGuide/CHAP_Storage.html)
+
+![RDS Instance Specs](img/instance_specs.png)
+
+The rest of the setting on this page request a DB identifier, username
+and a master password. We've chosen to use `gitlab-ha`, `gitlab` and a
+very secure password respectively. Keep these in hand for later.
+
+![Network and Security](img/rds-net-opt.png)
+
+Make sure to choose our gitlab VPC, our subnet group, not have it public,
+and to leave it to create a new security group. The only additional
+change which will be helpful is the database name for which we can use
+`gitlabhq_production`.
+
+***
+
+## ElastiCache
+
+EC is an in-memory hosted caching solution. Redis maintains its own
+persistance and is used for certain types of application.
+
+Let's choose the ElastiCache service in the Database section from our
+AWS console. Now lets create a cache subnet group which will be very
+similar to the RDS subnet group. Make sure to select our VPC and its
+private subnets.
+
+![ElastiCache](img/ec-subnet.png)
+
+Now press the Launch a Cache Cluster and choose Redis for our
+DB engine. You'll be able to configure details such as replication,
+Multi AZ and node types. The second section will allow us to choose our
+subnet and security group and
+
+![Redis Cluster details](img/redis-cluster-det.png)
+
+![Redis Network](img/redis-net.png)
+
+***
+
+## Elastic File System
+
+This new AWS offering allows us to create a file system accessible by

+EC2 instances within a VPC. Choose our VPC and the subnets will be
+
automatically configured assuming we don't need to set explicit IPs.
+The
next section allows us to add tags and choose between General
+Purpose or
Max I/O which is a good option when being accessed by a
+large number of
EC2 instances.
+
+

![Elastic File System](img/elastic-file-system.png)
+
+To actually mount and install the NFS client we'll use the User Data
+section when adding our Launch Configuration.
+
+***
+
+## Initiate AMI
+
+We are going to launch an EC2 instance and bake an image so that we can
+later use it for auto scaling. We'll also take this opportunity to add an
+extension to our RDS through this temporary EC2 instance.
+
+### EC2 Instance
+
+Look for the EC2 option and choose to create an instance. We'll need at
+least a t2.medium type and for this article we'll choose an Ubuntu 14.04
+HVM 64-bit. In the Configure Instance section choose our GitLab VPC and
+a public subnet. I'd choose at least 10GB of storage.
+
+In the security group we'll create a new one considering that we need to
+SSH into the instance and also try it out through http. So let's add the
+http traffic from anywhere and name it something such as
+`gitlab-ec2-security-group`.
+
+While we wait for it to launch we can allocate an Elastic IP and
+associate it with our new EC2 instance.
+
+### RDS and Redis Security Group
+
+After the instance is being created we will navigate to our EC2 security
+groups and add a small change for our EC2 instances to be able to
+connect to RDS. First copy the security group name we just defined,
+namely `gitlab-ec2-security-group`, and edit select the RDS security
+group and edit the inbound rules. Choose the rule type to be PostgreSQL
+and paste the name under source.
+
+![RDS security group](img/rds-sec-group.png)
+
+Similar to the above we'll jump to the `gitlab-ec2-security-group` group
+and add a custom TCP rule for port 6379 accessible within itself.
+
+### Install GitLab
+
+To connect through SSH you will need to have the `pem` file which you
+chose available and with the correct permissions such as `400`.
+
+After accessing your server don't forget to update and upgrade your
+packages.
+
+ sudo apt-get update && sudo apt-get upgrade -y
+
+Then follow installation instructions from
+[GitLab](https://about.gitlab.com/downloads-ee/#ubuntu1404), but before
+running reconfigure we need to make sure all our services are tied down
+so just leave the reconfigure command until after we edit our gitlab.rb
+file.
+
+
+### Extension for PostgreSQL
+
+Connect to your new RDS instance to verify access and to install
+a required extension. We can find the host or endpoint by selecting the
+instance and we just created and after the details drop down we'll find
+it labeled as 'Endpoint'; do remember not to include the colon and port
+number.
+
+ sudo /opt/gitlab/embedded/bin/psql -U gitlab -h <rds-endpoint> -d gitlabhq_production
+ psql (9.4.7)
+ Type "help" for help.
+
+ gitlab=# CREATE EXTENSION pg_trgm;
+ gitlab=# \q
+
+### Configure GitLab
+
+While connected to your server edit the `gitlab.rb` file at `/etc/gitlab/gitlab.rb`
+find the `external_url 'http://gitlab.example.com'` option and change it
+to the domain you will be using or the public IP address of the current
+instance to test the configuration.
+
+For a more detailed description about configuring GitLab read [Configuring GitLab for HA](http://docs.gitlab.com/ee/administration/high_availability/gitlab.html)
+
+Now look for the GitLab database settings and uncomment as necessary. In
+our current case we'll specify the adapter, encoding, host, db name,
+username, and password.
+
+ gitlab_rails['db_adapter'] = "postgresql"
+ gitlab_rails['db_encoding'] = "unicode"
+ gitlab_rails['db_database'] = "gitlabhq_production"
+ gitlab_rails['db_username'] = "gitlab"
+ gitlab_rails['db_password'] = "mypassword"
+ gitlab_rails['db_host'] = "<rds-endpoint>"
+
+Next we only need to configure the Redis section by adding the host and
+uncommenting the port.
+
+
+
+The last configuration step is to [change the default file locations ](http://docs.gitlab.com/ee/administration/high_availability/nfs.html)
+to make the EFS integration easier to manage.
+
+ gitlab_rails['redis_host'] = "<redis-endpoint>"
+ gitlab_rails['redis_port'] = 6379
+
+Finally run reconfigure, you might find it useful to run a check and
+a service status to make sure everything has been setup correctly.
+
+ sudo gitlab-ctl reconfigure
+ sudo gitlab-rake gitlab:check
+ sudo gitlab-ctl status
+
+If everything looks good copy the Elastic IP over to your browser and
+test the instance manually.
+
+### AMI
+
+After you finish testing your EC2 instance go back to its dashboard and
+while the instance is selected press on the Actions dropdown to choose
+Image -> Create an Image. Give it a name and description and confirm.
+
+***
+
+## Load Balancer
+
+On the same dashboard look for Load Balancer on the left column and press
+the Create button. Choose a classic Load Balancer, our gitlab VPC, not
+internal and make sure its listening for HTTP and HTTPS on port 80.
+
+Here is a tricky part though, when adding subnets we need to associate
+public subnets instead of the private ones where our instances will
+actually live.
+
+On the secruity group section let's create a new one named
+`gitlab-loadbalancer-sec-group` and allow both HTTP ad HTTPS traffic
+from anywhere.
+
+The Load Balancer Health will allow us to indicate where to ping and what
+makes up a healthy or unhealthy instance.
+
+We won't add the instance on the next session because we'll destroy it
+momentarily as we'll be using the image we where creating. We will keep
+the Enable Cross-Zone and Enable Connection Draining active.
+
+After we finish creating the Load Balancer we can re visit our Security
+Groups to improve access only through the ELB and any other requirement
+you might have.
+
+***
+
+## Auto Scaling Group
+
+Our AMI should be done by now so we can start working on our Auto
+Scaling Group.
+
+This option is also available through the EC2 dashboard on the left
+sidebar. Press on the create button. Select the new image on My AMIs and
+give it a `t2.medium` size. To be able to use Elastic File System we need
+to add a script to mount EFS automatically at launch. We'll do this at
+the Advanced Details section where we have a [User Data](http://docs.aws.amazon.com/AWSEC2/latest/UserGuide/user-data.html)
+text area that allows us to add a lot of custom configurations which
+allows you to add a custom script for when launching an instance. Let's
+add the following script to the User Data section:
+
+
+ #cloud-config
+ package_upgrade: true
+ packages:
+ - nfs-common
+ runcmd:
+ - mkdir -p /gitlab-data
+ - chown ec2-user:ec2-user /gitlab-data
+ - echo "$(curl --silent http://169.254.169.254/latest/meta-data/placement/availability-zone).file-system-id.aws-region.amazonaws.com:/ /gitlab-data nfs defaults,vers=4.1 0 0" >> /etc/fstab
+ - mount -a -t nfs
+ - sudo gitlab-ctl reconfigure
+
+On the security group section we can chosse our existing
+`gitlab-ec2-security-group` group which has already been tested.
+
+After this is launched we are able to start creating our Auto Scaling
+Group. Start by giving it a name and assinging it our VPC and private
+subnets. We also want to always start with two instances and if you
+scroll down to Advanced Details we can choose to receive traffic from ELBs.
+Lets enable that option and select our ELB. We also want to use the ELB's
+health check.
+
+![Auto scaling](img/auto-scaling-det.png)
+
+### Policies
+
+This is the really great part of Auto Scaling, we get to choose when AWS
+launches new instances and when it removes them. For this group we'll
+scale between 2 and 4 instances where one instance will be added if CPU
+utilization is greater than 60% and one instance is removed if it falls
+to less than 45%. Here are the complete policies:
+
+![Policies](img/policies.png)
+
+You'll notice that after we save this AWS starts launching our two
+instances in different AZs and without a public IP which is exactly what
+we where aiming for.
+
+***
+
+## Final Thoughts
+
+After you're done with the policies section have some fun trying to break
+instances. You should be able to see how the Auto Scaling Group and the
+EC2 screen start bringing them up again.
+
+High Availability is a very big area, we went mostly through scaling and
+some redundancy options but it might also imply Geographic replication.
+There is a lot of ground yet to cover so have a read through these other
+resources and feel free to open an issue to request additional material.
+
+ * [GitLab High Availability](http://docs.gitlab.com/ce/administration/high_availability/README.html#sts=High Availability)
+ * [GitLab Geo](http://docs.gitlab.com/ee/gitlab-geo/README.html)
diff --git a/doc/university/high-availability/aws/img/auto-scaling-det.png b/doc/university/high-availability/aws/img/auto-scaling-det.png
new file mode 100644
index 00000000000..e9b65529495
--- /dev/null
+++ b/doc/university/high-availability/aws/img/auto-scaling-det.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/db-subnet-group.png b/doc/university/high-availability/aws/img/db-subnet-group.png
new file mode 100644
index 00000000000..0768aa73c45
--- /dev/null
+++ b/doc/university/high-availability/aws/img/db-subnet-group.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ec-subnet.png b/doc/university/high-availability/aws/img/ec-subnet.png
new file mode 100644
index 00000000000..f41d78b271d
--- /dev/null
+++ b/doc/university/high-availability/aws/img/ec-subnet.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/elastic-file-system.png b/doc/university/high-availability/aws/img/elastic-file-system.png
new file mode 100644
index 00000000000..7de866d1e89
--- /dev/null
+++ b/doc/university/high-availability/aws/img/elastic-file-system.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ig-rt.png b/doc/university/high-availability/aws/img/ig-rt.png
new file mode 100644
index 00000000000..93bb0c2ae02
--- /dev/null
+++ b/doc/university/high-availability/aws/img/ig-rt.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/ig.png b/doc/university/high-availability/aws/img/ig.png
new file mode 100644
index 00000000000..cc50456370f
--- /dev/null
+++ b/doc/university/high-availability/aws/img/ig.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/instance_specs.png b/doc/university/high-availability/aws/img/instance_specs.png
new file mode 100644
index 00000000000..ef31dc41dae
--- /dev/null
+++ b/doc/university/high-availability/aws/img/instance_specs.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/new_vpc.png b/doc/university/high-availability/aws/img/new_vpc.png
new file mode 100644
index 00000000000..4aac6af7c7a
--- /dev/null
+++ b/doc/university/high-availability/aws/img/new_vpc.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/policies.png b/doc/university/high-availability/aws/img/policies.png
new file mode 100644
index 00000000000..8c58117e4fa
--- /dev/null
+++ b/doc/university/high-availability/aws/img/policies.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/rds-net-opt.png b/doc/university/high-availability/aws/img/rds-net-opt.png
new file mode 100644
index 00000000000..bc204de2474
--- /dev/null
+++ b/doc/university/high-availability/aws/img/rds-net-opt.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/rds-sec-group.png b/doc/university/high-availability/aws/img/rds-sec-group.png
new file mode 100644
index 00000000000..8864dc3e463
--- /dev/null
+++ b/doc/university/high-availability/aws/img/rds-sec-group.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/redis-cluster-det.png b/doc/university/high-availability/aws/img/redis-cluster-det.png
new file mode 100644
index 00000000000..9e9a81283c5
--- /dev/null
+++ b/doc/university/high-availability/aws/img/redis-cluster-det.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/redis-net.png b/doc/university/high-availability/aws/img/redis-net.png
new file mode 100644
index 00000000000..037bd6d6897
--- /dev/null
+++ b/doc/university/high-availability/aws/img/redis-net.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/route_table.png b/doc/university/high-availability/aws/img/route_table.png
new file mode 100644
index 00000000000..1dea322474d
--- /dev/null
+++ b/doc/university/high-availability/aws/img/route_table.png
Binary files differ
diff --git a/doc/university/high-availability/aws/img/subnet.png b/doc/university/high-availability/aws/img/subnet.png
new file mode 100644
index 00000000000..dbc71201992
--- /dev/null
+++ b/doc/university/high-availability/aws/img/subnet.png
Binary files differ
diff --git a/doc/university/process/README.md b/doc/university/process/README.md
new file mode 100644
index 00000000000..7ff53c2cc3f
--- /dev/null
+++ b/doc/university/process/README.md
@@ -0,0 +1,30 @@
+---
+title: University | Process
+---
+
+## Suggesting improvements
+
+If you would like to teach a class or participate or help in any way please
+submit a merge request and assign it to [Job](https://gitlab.com/u/JobV).
+
+If you have suggestions for additional courses you would like to see,
+please submit a merge request to add an upcoming class, assign to
+[Chad](https://gitlab.com/u/chadmalchow) and /cc [Job](https://gitlab.com/u/JobV).
+
+## Adding classes
+
+1. All training materials of any kind should be added to [GitLab CE](https://gitlab.com/gitlab-org/gitlab-ce/)
+ to ensure they are available to a broad audience (don't use any other repo or
+ storage for training materials).
+1. Don't make materials that are needlessly specific to one group of people, try
+ to keep the wording broad and inclusive (don't make things for only GitLab Inc.
+ people, only interns, only customers, etc.).
+1. To allow people to contribute all content should be in git.
+1. The content can go in a subdirectory under `/doc/university/`.
+1. To make, view or modify the slides of the classes use [Deckset](http://www.decksetapp.com/)
+ or [RevealJS](http://lab.hakim.se/reveal-js/). Do not use Powerpoint or Google
+ Slides since this prevents everyone from contributing.
+1. Please upload any video recordings to our Youtube channel. We prefer them to
+ be public, if needed they can be unlisted but if so they should be linked from
+ this page.
+1. Please create a merge request and assign to [SeanPackham](https://gitlab.com/u/SeanPackham).
diff --git a/doc/university/support/README.md b/doc/university/support/README.md
new file mode 100644
index 00000000000..da991e56370
--- /dev/null
+++ b/doc/university/support/README.md
@@ -0,0 +1,188 @@
+
+## Support Boot Camp
+
+**Goal:** Prepare new Service Engineers at GitLab
+
+For each stage there are learning goals and content to support the learning of the engineer.
+The goal of this boot camp is to have every Service Engineer prepared to help our customers
+with whatever needs they might have and to also assist our awesome community with their
+questions.
+
+Always start with the [University Overview](../README.md) and then work
+your way here for more advanced and specific training. Once you feel comfortable
+with the topics of the current stage, move to the next.
+
+### Stage 1
+
+Follow the topics on the [University Overview](../README.md), concentrate on it
+during your first Stage, but also:
+
+- Perform the [first steps](https://about.gitlab.com/handbook/support/onboarding/#first-steps) of
+ the on-boarding process for new Service Engineers
+
+#### Goals
+
+Aim to have a good overview of the Product and main features, Git and the Company
+
+### Stage 2
+
+Continue to look over remaining portions of the [University Overview](../README.md) and continue on to these topics:
+
+#### Set up your development machine
+
+Get your development machine ready to familiarize yourself with the codebase, the components, and to be prepared to reproduce issues that our users encounter
+
+- Install the [GDK](https://gitlab.com/gitlab-org/gitlab-development-kit)
+ - [Setup OpenLDAP as part of this](https://gitlab.com/gitlab-org/gitlab-development-kit#openldap)
+
+#### Become comfortable with the Installation processes that we support
+
+It's important to understand how to install GitLab in the same way that our users do. Try installing different versions and upgrading and downgrading between them. Installation from source will give you a greater understanding of the components that we employ and how everything fits together.
+
+Sometimes we need to upgrade customers from old versions of GitLab to latest, so it's good to get some experience of doing that now.
+
+- [Installation Methods](https://about.gitlab.com/installation/):
+ - [Omnibus](https://gitlab.com/gitlab-org/omnibus-gitlab/)
+ - [Docker](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/docker)
+ - [Source](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/install/installation.md)
+- Get yourself a Digital Ocean droplet, where you can install and maintain your own instance of GitLab
+ - Ask in #infrastructure about this
+ - Populate with some test data
+ - Keep this up-to-date as patch and version releases become available, just like our customers would
+- Try out the following installation path
+ - [Install GitLab 4.2 from source](https://gitlab.com/gitlab-org/gitlab-ce/blob/d67117b5a185cfb15a1d7e749588ff981ffbf779/doc/install/installation.md)
+ - External MySQL database
+ - External NGINX
+ - Create some test data
+ - Populated Repos
+ - Users
+ - Groups
+ - Projects
+ - [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
+ - [Upgrade to 5.0 source using our Upgrade documentation](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/4.2-to-5.0.md)
+ - [Upgrade to 5.1 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.0-to-5.1.md)
+ - [Upgrade to 6.0 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/5.1-to-6.0.md)
+ - [Upgrade to 7.14 source](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/doc/update/6.x-or-7.x-to-7.14.md)
+ - [Backup using our Backup rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#create-a-backup-of-the-gitlab-system)
+ - [Perform the MySQL to PostgreSQL migration to convert your backup](http://docs.gitlab.com/ce/update/mysql_to_postgresql.html#converting-a-gitlab-backup-file-from-mysql-to-postgres)
+ - [Upgrade to Omnibus 7.14](http://doc.gitlab.com/omnibus/update/README.html#upgrading-from-a-non-omnibus-installation-to-an-omnibus-installation)
+ - [Restore backup using our Restore rake task](http://docs.gitlab.com/ce/raketasks/backup_restore.html#restore-a-previously-created-backup)
+ - [Upgrade to latest EE](https://about.gitlab.com/downloads-ee)
+ - (GitLab inc. only) Acquire and apply a license for the Enterprise Edition product, ask in #support
+- Perform a downgrade from [EE to CE](http://doc.gitlab.com/ee/downgrade_ee_to_ce/README.html)
+
+#### Start to learn about some of the integrations that we support
+
+Our integrations add great value to GitLab. User questions often relate to integrating GitLab with existing external services and the configuration involved
+
+- Learn about our Integrations (specially, not only):
+ - [LDAP](http://doc.gitlab.com/ee/integration/ldap.html)
+ - [JIRA](http://doc.gitlab.com/ee/project_services/jira.html)
+ - [Jenkins](http://doc.gitlab.com/ee/integration/jenkins.html)
+ - [SAML](http://doc.gitlab.com/ce/integration/saml.html)
+
+#### Goals
+
+- Aim to be comfortable with installation of the GitLab product and configuration of some of the major integrations
+- Aim to have an installation available for reproducing customer reports
+
+### Stage 3
+
+#### Understand the gathering of diagnostics for GitLab instances
+
+- Learn about the GitLab checks that are available
+ - [Environment Information and maintenance checks](http://docs.gitlab.com/ce/raketasks/maintenance.html)
+ - [GitLab check](http://docs.gitlab.com/ce/raketasks/check.html)
+ - Omnibus commands
+ - [Status](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#get-service-status)
+ - [Starting and stopping services](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#starting-and-stopping)
+ - [Starting a rails console](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/doc/maintenance/README.md#invoking-rake-tasks)
+
+#### Learn about the Support process
+
+Zendesk is our Support Centre and our main communication line with our Customers. We communicate with customers through several other channels too
+
+- Familiarize yourself with ZenDesk
+ - [UI Overview](https://support.zendesk.com/hc/en-us/articles/203661806-Introduction-to-the-Zendesk-agent-interface)
+ - [Updating Tickets](https://support.zendesk.com/hc/en-us/articles/212530318-Updating-and-solving-tickets)
+ - [Working w/ Tickets](https://support.zendesk.com/hc/en-us/articles/203690856-Working-with-tickets) *Read: avoiding agent collision.*
+- Dive into our ZenDesk support process by reading how to [handle tickets](https://about.gitlab.com/handbook/support/onboarding/#handling-tickets)
+- Start getting real world experience by handling real tickets, all the while gaining further experience with the Product.
+ - First, learn about our [Support Channels](https://about.gitlab.com/handbook/support/#support-channels)
+ - Ask other Service Engineers for help, when necessary, and to review your responses
+ - Start with [StackOverflow](https://about.gitlab.com/handbook/support/#stack-overflowa-namestack-overflowa) and the [GitLab forum](https://about.gitlab.com/handbook/support/#foruma-namegitlab-foruma)
+ - Here you will find a large variety of queries mainly from our Users who are self hosting GitLab CE
+ - Understand the questions that are asked and dig in to try to find a solution
+ - [Proceed on to the GitLab.com Support Forum](https://about.gitlab.com/handbook/support/#gitlabcom-support-trackera-namesupp-foruma)
+ - Here you will find queries regarding our own GitLab.com
+ - Helping Users here will give you an understanding of our Admin interface and other tools
+ - [Proceed on to the Twitter tickets in Zendesk](https://about.gitlab.com/handbook/support/#twitter)
+ - Here you will gain a great insight into our userbase
+ - Learn from any complaints and problems and feed them back to the team
+ - Tweets can range from help needed with GitLab installations, the API and just general queries
+ - [Proceed on to Regular email Support tickets](https://about.gitlab.com/handbook/support/#regular-zendesk-tickets-a-nameregulara)
+ - Here you will find tickets from our GitLab EE Customers and GitLab CE Users
+ - Tickets here are extremely varied and often very technical
+ - You should be prepared for these tickets, given the knowledge gained from previous tiers and your training
+- Check out your colleagues' responses
+ - Hop on to the #support-live-feed channel in Slack and see the tickets as they come in and are updated
+ - Read through old tickets that your colleagues have worked on
+- Start arranging to pair on calls with other Service Engineers. Aim to cover a few of each type of call
+ - [Learn about Cisco WebEx](https://about.gitlab.com/handbook/support/onboarding/#webexa-namewebexa)
+ - Training calls
+ - Information gathering calls
+ - It's good to find out how new and prospective customers are going to be using the product and how they will set up their infrastructure
+ - Diagnosis calls
+ - When email isn't enough we may need to hop on a call and do some debugging along side the customer
+ - These paired calls are a great learning experience
+ - Upgrade calls
+ - Emergency calls
+
+#### Learn about the Escalation process for tickets
+
+Some tickets need specific knowledge or a deep understanding of a particular component and will need to be escalated to a Senior Service Engineer or Developer
+
+- Read about [Escalation](https://about.gitlab.com/handbook/support/onboarding/#create-issuesa-namecreate-issuea)
+- Find the macros in Zendesk for ticket escalations
+- Take a look at the [GitLab.com Team page](https://about.gitlab.com/team/) to find the resident experts in their fields
+
+#### Learn about raising issues and fielding feature proposals
+
+- Understand what's in the pipeline and proposed features at GitLab: [Direction Page](https://about.gitlab.com/direction/)
+- Practice searching issues and filtering using [labels](https://gitlab.com/gitlab-org/gitlab-ce/labels) to find existing feature proposals and bugs
+- If raising a new issue always provide a relevant label and a link to the relevant ticket in Zendesk
+- Add [customer labels](https://gitlab.com/gitlab-org/gitlab-ce/issues?label_name%5B%5D=customer) for those issues relevant to our subscribers
+- Take a look at the [existing issue templates](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/CONTRIBUTING.md#issue-tracker) to see what is expected
+- Raise issues for bugs in a manner that would make the issue easily reproducible. A Developer or a contributor may work on your issue
+
+#### Goals
+
+- Aim to have a good understanding of the problems that customers are facing
+- Aim to have gained experience in scheduling and participating in calls with customers
+- Aim to have a good understanding of ticket flow through Zendesk and how to interat with our various channels
+
+### Stage 4
+
+#### Advanced GitLab topics
+
+Move on to understanding some of GitLab's more advanced features. You can make use of GitLab.com to understand the features from an end-user perspective and then use your own instance to understand setup and configuration of the feature from an Administrative perspective
+
+- Set up and try [Git Annex](http://doc.gitlab.com/ee/workflow/git_annex.html)
+- Set up and try [Git LFS](http://doc.gitlab.com/ee/workflow/lfs/manage_large_binaries_with_git_lfs.html)
+- Get to know the [GitLab API](http://doc.gitlab.com/ee/api/README.html), its capabilities and shortcomings
+- Learn how to [migrate from SVN to Git](http://doc.gitlab.com/ee/workflow/importing/migrating_from_svn.html)
+- Set up [GitLab CI](http://doc.gitlab.com/ee/ci/quick_start/README.html)
+- Create your first [GitLab Page](http://doc.gitlab.com/ee/pages/administration.html)
+- Get to know the GitLab Codebase by reading through the source code:
+ - Find the differences between the [EE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
+ and the [CE codebase](https://gitlab.com/gitlab-org/gitlab-ce)
+- Ask as many questions as you can think of on the `#support` chat channel
+
+#### Get initiated for on-call duty
+
+- Read over the [public run-books to understand common tasks](https://gitlab.com/gitlab-com/runbooks)
+- Create an issue on the internal Organization tracker to schedule time with the DevOps / Production team, so that you learn how to handle GitLab.com going down. Once you are trained for this, you are ready to be added to the on-call rotation.
+
+#### Goals
+
+- Aim to become a fully-fledged Service Engineer!
diff --git a/doc/university/training/end-user/README.md b/doc/university/training/end-user/README.md
new file mode 100644
index 00000000000..03c62a81b10
--- /dev/null
+++ b/doc/university/training/end-user/README.md
@@ -0,0 +1,420 @@
+
+# Training
+
+This training material is the markdown used to generate training slides
+which can be found at [End User Slides](https://gitlab-org.gitlab.io/end-user-training-slides/#/)
+through it's [RevealJS](https://gitlab.com/gitlab-org/end-user-training-slides)
+project.
+
+---
+
+## Git Intro
+
+---
+
+### What is a Version Control System (VCS)
+
+- Records changes to a file
+- Maintains history of changes
+- Disaster Recovery
+- Types of VCS: Local, Centralized and Distributed
+
+---
+
+### Short Story of Git
+
+- 1991-2002: The Linux kernel was being maintaned by sharing archived files
+ and patches.
+- 2002: The Linux kernel project began using a DVCS called BitKeeper
+- 2005: BitKeeper revoked the free-of-charge status and Git was created
+
+---
+
+### What is Git
+
+- Distributed Version Control System
+- Great branching model that adapts well to most workflows
+- Fast and reliable
+- Keeps a complete history
+- Disaster recovery friendly
+- Open Source
+
+---
+
+### Getting Help
+
+- Use the tools at your disposal when you get stuck.
+ - Use `git help <command>` command
+ - Use Google (i.e. StackOverflow, Google groups)
+ - Read documentation at https://git-scm.com
+
+---
+
+## Git Setup
+Workshop Time!
+
+---
+
+### Setup
+
+- Windows: Install 'Git for Windows'
+ - https://git-for-windows.github.io
+- Mac: Type `git` in the Terminal application.
+ - If it's not installed, it will prompt you to install it.
+- Linux
+ - Debian: `sudo apt-get install git-all`
+ - Red Hat `sudo yum install git-all`
+
+---
+
+### Configure
+
+- One-time configuration of the Git client:
+
+```bash
+git config --global user.name "Your Name"
+git config --global user.email you@example.com
+```
+
+- If you don't use the global flag you can setup a different author for
+ each project
+- Check settings with:
+
+```bash
+git config --global --list
+```
+- You might want or be required to use an SSH key.
+ - Instructions: [SSH](http://doc.gitlab.com/ce/ssh/README.html)
+
+---
+
+### Workspace
+
+- Choose a directory on you machine easy to access
+- Create a workspace or development directory
+- This is where we'll be working and adding content
+
+---
+
+```bash
+mkdir ~/development
+cd ~/development
+
+-or-
+
+mkdir ~/workspace
+cd ~/workspace
+```
+
+---
+
+## Git Basics
+
+---
+
+### Git Workflow
+
+- Untracked files
+ - New files that Git has not been told to track previously.
+- Working area (Workspace)
+ - Files that have been modified but are not committed.
+- Staging area (Index)
+ - Modified files that have been marked to go in the next commit.
+- Upstream
+ - Hosted repository on a shared server
+
+---
+
+### GitLab
+
+- GitLab is an application to code, test and deploy.
+- Provides repository management with access controls, code reviews,
+ issue tracking, Merge Requests, and other features.
+- The hosted version of GitLab is gitlab.com
+
+---
+
+### New Project
+
+- Sign in into your gitlab.com account
+- Create a project
+- Choose to import from 'Any Repo by URL' and use https://gitlab.com/gitlab-org/training-examples.git
+- On your machine clone the `training-examples` project
+
+---
+
+### Git and GitLab basics
+
+1. Edit `edit_this_file.rb` in `training-examples`
+2. See it listed as a changed file (working area)
+3. View the differences
+4. Stage the file
+5. Commit
+6. Push the commit to the remote
+7. View the git log
+
+---
+
+```shell
+# Edit `edit_this_file.rb`
+git status
+git diff
+git add <file>
+git commit -m 'My change'
+git push origin master
+git log
+```
+
+---
+
+### Feature Branching
+
+1. Create a new feature branch called `squash_some_bugs`
+2. Edit `bugs.rb` and remove all the bugs.
+3. Commit
+4. Push
+
+---
+
+```shell
+git checkout -b squash_some_bugs
+# Edit `bugs.rb`
+git status
+git add bugs.rb
+git commit -m 'Fix some buggy code'
+git push origin squash_some_bugs
+```
+
+---
+
+## Merge Request
+
+---
+
+### Merge requests
+
+- When you want feedback create a merge request
+- Target is the ‘default’ branch (usually master)
+- Assign or mention the person you would like to review
+- Add `WIP` to the title if it's a work in progress
+- When accepting, always delete the branch
+- Anyone can comment, not just the assignee
+- Push corrections to the same branch
+
+
+---
+
+### Merge request example
+
+- Create your first merge request
+ - Use the blue button in the activity feed
+ - View the diff (changes) and leave a comment
+ - Push a new commit to the same branch
+ - Review the changes again and notice the update
+
+---
+
+### Feedback and Collaboration
+
+- Merge requests are a time for feedback and collaboration
+- Giving feedback is hard
+- Be as kind as possible
+- Receiving feedback is hard
+- Be as receptive as possible
+- Feedback is about the best code, not the person. You are not your code
+- Feedback and Collaboration
+
+---
+
+### Feedback and Collaboration
+
+- Review the Thoughtbot code-review guide for suggestions to follow when reviewing merge requests:[Thoughtbot](https://github.com/thoughtbot/guides/tree/master/code-review)
+- See GitLab merge requests for examples: [Merge Requests](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests)
+
+---
+
+## Merge Conflicts
+
+---
+
+### Merge Conflicts
+* Happen often
+* Learning to fix conflicts is hard
+* Practice makes perfect
+* Force push after fixing conflicts. Be careful!
+
+---
+
+### Example Plan
+1. Checkout a new branch and edit conflicts.rb. Add 'Line4' and 'Line5'.
+2. Commit and push
+3. Checkout master and edit conflicts.rb. Add 'Line6' and 'Line7' below 'Line3'.
+4. Commit and push to master
+5. Create a merge request and watch it fail
+6. Rebase our new branch with master
+7. Fix conflicts on the conflicts.rb file.
+8. Stage the file and continue rebasing
+9. Force push the changes
+10. Finally continue with the Merge Request
+
+---
+
+### Example 1/2
+
+ git checkout -b conflicts_branch
+
+ # vi conflicts.rb
+ # Add 'Line4' and 'Line5'
+
+ git commit -am "add line4 and line5"
+ git push origin conflicts_branch
+
+ git checkout master
+
+ # vi conflicts.rb
+ # Add 'Line6' and 'Line7'
+ git commit -am "add line6 and line7"
+ git push origin master
+
+---
+
+### Example 2/2
+
+Create a merge request on the GitLab web UI. You'll see a conflict warning.
+
+ git checkout conflicts_branch
+ git fetch
+ git rebase master
+
+ # Fix conflicts by editing the files.
+
+ git add conflicts.rb
+ # No need to commit this file
+
+ git rebase --continue
+
+ # Remember that we have rewritten our commit history so we
+ # need to force push so that our remote branch is restructured
+ git push origin conflicts_branch -f
+
+---
+
+### Notes
+
+* When to use `git merge` and when to use `git rebase`
+* Rebase when updating your branch with master
+* Merge when bringing changes from feature to master
+* Reference: https://www.atlassian.com/git/tutorials/merging-vs-rebasing/
+
+---
+
+## Revert and Unstage
+
+---
+
+### Unstage
+
+To remove files from stage use reset HEAD. Where HEAD is the last commit of the current branch:
+
+ git reset HEAD <file>
+
+This will unstage the file but maintain the modifications. To revert the file back to the state it was in before the changes we can use:
+
+ git checkout -- <file>
+
+To remove a file from disk and repo use 'git rm' and to rm a dir use the '-r' flag:
+
+ git rm '*.txt'
+ git rm -r <dirname>
+
+If we want to remove a file from the repository but keep it on disk, say we forgot to add it to our .gitignore file then use `--cache`:
+
+ git rm <filename> --cache
+
+---
+
+### Undo Commits
+
+Undo last commit putting everything back into the staging area:
+
+ git reset --soft HEAD^
+
+Add files and change message with:
+
+ git commit --amend -m "New Message"
+
+Undo last and remove changes
+
+ git reset --hard HEAD^
+
+Same as last one but for two commits back:
+
+ git reset --hard HEAD^^
+
+Don't reset after pushing
+
+---
+
+### Reset Workflow
+
+1. Edit file again 'edit_this_file.rb'
+2. Check status
+3. Add and commit with wrong message
+4. Check log
+5. Amend commit
+6. Check log
+7. Soft reset
+8. Check log
+9. Pull for updates
+10. Push changes
+
+----
+
+ # Change file edit_this_file.rb
+ git status
+ git commit -am "kjkfjkg"
+ git log
+ git commit --amend -m "New comment added"
+ git log
+ git reset --soft HEAD^
+ git log
+ git pull origin master
+ git push origin master
+
+---
+
+### Note
+
+git revert vs git reset
+Reset removes the commit while revert removes the changes but leaves the commit
+Revert is safer considering we can revert a revert
+
+
+ # Changed file
+ git commit -am "bug introduced"
+ git revert HEAD
+ # New commit created reverting changes
+ # Now we want to re apply the reverted commit
+ git log # take hash from the revert commit
+ git revert <rev commit hash>
+ # reverted commit is back (new commit created again)
+
+---
+
+## Questions
+
+---
+
+## Instructor Notes
+
+---
+
+### Version Control
+ - Local VCS was used with a filesystem or a simple db.
+ - Centralized VCS such as Subversion includes collaboration but
+ still is prone to data loss as the main server is the single point of
+ failure.
+ - Distributed VCS enables the team to have a complete copy of the project
+ and work with little dependency to the main server. In case of a main
+ server failing the project can be recovered by any of the latest copies
+ from the team
diff --git a/doc/update/8.0-to-8.1.md b/doc/update/8.0-to-8.1.md
index d57c0d0674d..bfb83cf79b1 100644
--- a/doc/update/8.0-to-8.1.md
+++ b/doc/update/8.0-to-8.1.md
@@ -99,6 +99,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 7. Update configuration files
diff --git a/doc/update/8.1-to-8.2.md b/doc/update/8.1-to-8.2.md
index 46dfa2232b4..7f36ce00e96 100644
--- a/doc/update/8.1-to-8.2.md
+++ b/doc/update/8.1-to-8.2.md
@@ -116,6 +116,10 @@ sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS
# Update init.d script
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 7. Update configuration files
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index b058f8e2a03..119c5f475e4 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby
-If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
@@ -156,6 +158,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index 076696f565b..07743d050f7 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -22,7 +22,9 @@ sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
### 3. Update Ruby
-If you are you running Ruby 2.1.x, you do not _need_ to upgrade Ruby yet, but you should note that support for 2.1.x is deprecated and we will require 2.3.x in 8.13. It's strongly recommended that you upgrade as soon as possible.
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
You can check which version you are running with `ruby -v`.
@@ -164,6 +166,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
new file mode 100644
index 00000000000..00d63c1b3c6
--- /dev/null
+++ b/doc/update/8.12-to-8.13.md
@@ -0,0 +1,205 @@
+# From 8.12 to 8.13
+
+Make sure you view this update guide from the tag (version) of GitLab you would
+like to install. In most cases this should be the highest numbered production
+tag (without rc in it). You can select the tag in the version dropdown at the
+top left corner of GitLab (below the menu bar).
+
+If the highest number stable branch is unclear please check the
+[GitLab Blog](https://about.gitlab.com/blog/archives.html) for installation
+guide links by version.
+
+### 1. Stop server
+
+ sudo service gitlab stop
+
+### 2. Backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
+```
+
+### 3. Update Ruby
+
+We will continue supporting Ruby < 2.3 for the time being but we recommend you
+upgrade to Ruby 2.3 if you're running a source installation, as this is the same
+version that ships with our Omnibus package.
+
+You can check which version you are running with `ruby -v`.
+
+Download and compile Ruby:
+
+```bash
+mkdir /tmp/ruby && cd /tmp/ruby
+curl --remote-name --progress https://cache.ruby-lang.org/pub/ruby/2.3/ruby-2.3.1.tar.gz
+echo 'c39b4001f7acb4e334cb60a0f4df72d434bef711 ruby-2.3.1.tar.gz' | shasum --check - && tar xzf ruby-2.3.1.tar.gz
+cd ruby-2.3.1
+./configure --disable-install-rdoc
+make
+sudo make install
+```
+
+Install Bundler:
+
+```bash
+sudo gem install bundler --no-ri --no-rdoc
+```
+
+### 4. Get latest code
+
+```bash
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
+```
+
+For GitLab Community Edition:
+
+```bash
+sudo -u git -H git checkout 8-13-stable
+```
+
+OR
+
+For GitLab Enterprise Edition:
+
+```bash
+sudo -u git -H git checkout 8-13-stable-ee
+```
+
+### 5. Update gitlab-shell
+
+```bash
+cd /home/git/gitlab-shell
+sudo -u git -H git fetch --all --tags
+sudo -u git -H git checkout v3.6.3
+```
+
+### 6. Update gitlab-workhorse
+
+Install and compile gitlab-workhorse. This requires
+[Go 1.5](https://golang.org/dl) which should already be on your system from
+GitLab 8.1.
+
+```bash
+cd /home/git/gitlab-workhorse
+sudo -u git -H git fetch --all
+sudo -u git -H git checkout v0.8.4
+sudo -u git -H make
+```
+
+### 7. Install libs, migrations, etc.
+
+```bash
+cd /home/git/gitlab
+
+# MySQL installations (note: the line below states '--without postgres')
+sudo -u git -H bundle install --without postgres development test --deployment
+
+# PostgreSQL installations (note: the line below states '--without mysql')
+sudo -u git -H bundle install --without mysql development test --deployment
+
+# Optional: clean up old gems
+sudo -u git -H bundle clean
+
+# Run database migrations
+sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
+
+# Clean up assets and cache
+sudo -u git -H bundle exec rake assets:clean assets:precompile cache:clear RAILS_ENV=production
+```
+
+### 8. Update configuration files
+
+#### New configuration options for `gitlab.yml`
+
+There are new configuration options available for [`gitlab.yml`](config/gitlab.yml.example). View them with the command below and apply them manually to your current `gitlab.yml`:
+
+```sh
+git diff origin/8-12-stable:config/gitlab.yml.example origin/8-13-stable:config/gitlab.yml.example
+```
+
+#### Git configuration
+
+Configure Git to generate packfile bitmaps (introduced in Git 2.0) on
+the GitLab server during `git gc`.
+
+```sh
+sudo -u git -H git config --global repack.writeBitmaps true
+```
+
+#### Nginx configuration
+
+Ensure you're still up-to-date with the latest NGINX configuration changes:
+
+```sh
+# For HTTPS configurations
+git diff origin/8-12-stable:lib/support/nginx/gitlab-ssl origin/8-13-stable:lib/support/nginx/gitlab-ssl
+
+# For HTTP configurations
+git diff origin/8-12-stable:lib/support/nginx/gitlab origin/8-13-stable:lib/support/nginx/gitlab
+```
+
+If you are using Apache instead of NGINX please see the updated [Apache templates].
+Also note that because Apache does not support upstreams behind Unix sockets you
+will need to let gitlab-workhorse listen on a TCP port. You can do this
+via [/etc/default/gitlab].
+
+[Apache templates]: https://gitlab.com/gitlab-org/gitlab-recipes/tree/master/web-server/apache
+[/etc/default/gitlab]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/lib/support/init.d/gitlab.default.example#L38
+
+#### SMTP configuration
+
+If you're installing from source and use SMTP to deliver mail, you will need to add the following line
+to config/initializers/smtp_settings.rb:
+
+```ruby
+ActionMailer::Base.delivery_method = :smtp
+```
+
+See [smtp_settings.rb.sample] as an example.
+
+[smtp_settings.rb.sample]: https://gitlab.com/gitlab-org/gitlab-ce/blob/8-13-stable/config/initializers/smtp_settings.rb.sample#L13
+
+#### Init script
+
+Ensure you're still up-to-date with the latest init script changes:
+
+ sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
+
+### 9. Start application
+
+ sudo service gitlab start
+ sudo service nginx restart
+
+### 10. Check application status
+
+Check if GitLab and its environment are configured correctly:
+
+ sudo -u git -H bundle exec rake gitlab:env:info RAILS_ENV=production
+
+To make sure you didn't miss anything run a more thorough check:
+
+ sudo -u git -H bundle exec rake gitlab:check RAILS_ENV=production
+
+If all items are green, then congratulations, the upgrade is complete!
+
+## Things went south? Revert to previous version (8.12)
+
+### 1. Revert the code to the previous version
+
+Follow the [upgrade guide from 8.11 to 8.12](8.11-to-8.12.md), except for the
+database migration (the backup is already migrated to the previous version).
+
+### 2. Restore from the backup
+
+```bash
+cd /home/git/gitlab
+sudo -u git -H bundle exec rake gitlab:backup:restore RAILS_ENV=production
+```
+
+If you have more than one backup `*.tar` file(s) please add `BACKUP=timestamp_of_backup` to the command above.
diff --git a/doc/update/8.2-to-8.3.md b/doc/update/8.2-to-8.3.md
index 9f5c6c4dc84..dd3fdafd8d1 100644
--- a/doc/update/8.2-to-8.3.md
+++ b/doc/update/8.2-to-8.3.md
@@ -158,6 +158,10 @@ it where the 'public' directory of GitLab is.
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Use Redis v2.8.0+
diff --git a/doc/update/8.3-to-8.4.md b/doc/update/8.3-to-8.4.md
index 9f6517d9487..e62d894609a 100644
--- a/doc/update/8.3-to-8.4.md
+++ b/doc/update/8.3-to-8.4.md
@@ -98,6 +98,10 @@ We updated the init script for GitLab in order to set a specific PATH for gitlab
cd /home/git/gitlab
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
```
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.4-to-8.5.md b/doc/update/8.4-to-8.5.md
index 0cb137a03cc..678cc69d773 100644
--- a/doc/update/8.4-to-8.5.md
+++ b/doc/update/8.4-to-8.5.md
@@ -119,6 +119,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.5-to-8.6.md b/doc/update/8.5-to-8.6.md
index 6267f14eba4..a76346516b9 100644
--- a/doc/update/8.5-to-8.6.md
+++ b/doc/update/8.5-to-8.6.md
@@ -138,6 +138,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index cb66ef920bb..05ef4e61759 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.7-to-8.8.md b/doc/update/8.7-to-8.8.md
index 32906650f6f..8ce434e5f78 100644
--- a/doc/update/8.7-to-8.8.md
+++ b/doc/update/8.7-to-8.8.md
@@ -127,6 +127,10 @@ via [/etc/default/gitlab].
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 8. Start application
diff --git a/doc/update/8.8-to-8.9.md b/doc/update/8.8-to-8.9.md
index f078a2bece5..aa077316bbe 100644
--- a/doc/update/8.8-to-8.9.md
+++ b/doc/update/8.8-to-8.9.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/update/8.9-to-8.10.md b/doc/update/8.9-to-8.10.md
index a057a423e61..bb2c79fbb84 100644
--- a/doc/update/8.9-to-8.10.md
+++ b/doc/update/8.9-to-8.10.md
@@ -156,6 +156,10 @@ See [smtp_settings.rb.sample] as an example.
Ensure you're still up-to-date with the latest init script changes:
sudo cp lib/support/init.d/gitlab /etc/init.d/gitlab
+
+For Ubuntu 16.04.1 LTS:
+
+ sudo systemctl daemon-reload
### 9. Start application
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c7fda8a497f..56e5b802a52 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -27,6 +27,7 @@
* [Horizontal Rule](#horizontal-rule)
* [Line Breaks](#line-breaks)
* [Tables](#tables)
+* [Footnotes](#footnotes)
**[Wiki-Specific Markdown](#wiki-specific-markdown)**
@@ -699,6 +700,15 @@ By including colons in the header row, you can align the text within that column
| Cell 1 | Cell 2 | Cell 3 | Cell 4 | Cell 5 | Cell 6 |
| Cell 7 | Cell 8 | Cell 9 | Cell 10 | Cell 11 | Cell 12 |
+## Footnotes
+
+You can add footnotes to your text as follows.[^1]
+[^1]: This is my awesome footnote.
+
+```
+You can add footnotes to your text as follows.[^1]
+[^1]: This is my awesome footnote.
+```
## Wiki-specific Markdown
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 12d5b8f8744..c0dc80325b6 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -23,6 +23,7 @@ The following table depicts the various user permission levels in a project.
| See a list of builds | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| See a build log | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
| Download and browse build artifacts | ✓ [^1] | ✓ | ✓ | ✓ | ✓ |
+| View wiki pages | ✓ | ✓ | ✓ | ✓ | ✓ |
| Pull project code | | ✓ | ✓ | ✓ | ✓ |
| Download project | | ✓ | ✓ | ✓ | ✓ |
| Create code snippets | | ✓ | ✓ | ✓ | ✓ |
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
new file mode 100644
index 00000000000..b205fea2c40
--- /dev/null
+++ b/doc/user/project/container_registry.md
@@ -0,0 +1,253 @@
+# GitLab Container Registry
+
+> [Introduced][ce-4040] in GitLab 8.8.
+
+---
+
+> **Note**
+Docker Registry manifest `v1` support was added in GitLab 8.9 to support Docker
+versions earlier than 1.10.
+>
+This document is about the user guide. To learn how to enable GitLab Container
+Registry across your GitLab instance, visit the
+[administrator documentation](../../administration/container_registry.md).
+
+With the Docker Container Registry integrated into GitLab, every project can
+have its own space to store its Docker images.
+
+You can read more about Docker Registry at https://docs.docker.com/registry/introduction/.
+
+---
+
+## Enable the Container Registry for your project
+
+1. First, ask your system administrator to enable GitLab Container Registry
+ following the [administration documentation](../../administration/container_registry.md).
+ If you are using GitLab.com, this is enabled by default so you can start using
+ the Registry immediately.
+
+1. Go to your project's settings and enable the **Container Registry** feature
+ on your project. For new projects this might be enabled by default. For
+ existing projects (prior GitLab 8.8), you will have to explicitly enable it.
+
+ ![Enable Container Registry](img/container_registry_enable.png)
+
+1. Hit **Save changes** for the changes to take effect. You should now be able
+ to see the **Registry** link in the project menu.
+
+ ![Container Registry tab](img/container_registry_tab.png)
+
+## Build and push images
+
+If you visit the **Registry** link under your project's menu, you can see the
+explicit instructions to login to the Container Registry using your GitLab
+credentials.
+
+For example if the Registry's URL is `registry.example.com`, the you should be
+able to login with:
+
+```
+docker login registry.example.com
+```
+
+Building and publishing images should be a straightforward process. Just make
+sure that you are using the Registry URL with the namespace and project name
+that is hosted on GitLab:
+
+```
+docker build -t registry.example.com/group/project .
+docker push registry.example.com/group/project
+```
+
+Your image will be named after the following scheme:
+
+```
+<registry URL>/<namespace>/<project>
+```
+
+As such, the name of the image is unique, but you can differentiate the images
+using tags.
+
+## Use images from GitLab Container Registry
+
+To download and run a container from images hosted in GitLab Container Registry,
+use `docker run`:
+
+```
+docker run [options] registry.example.com/group/project [arguments]
+```
+
+For more information on running Docker containers, visit the
+[Docker documentation][docker-docs].
+
+## Control Container Registry from within GitLab
+
+GitLab offers a simple Container Registry management panel. Go to your project
+and click **Registry** in the project menu.
+
+This view will show you all tags in your project and will easily allow you to
+delete them.
+
+![Container Registry panel](img/container_registry_panel.png)
+
+## Build and push images using GitLab CI
+
+> **Note:**
+This feature requires GitLab 8.8 and GitLab Runner 1.2.
+
+Make sure that your GitLab Runner is configured to allow building Docker images by
+following the [Using Docker Build](../ci/docker/using_docker_build.md)
+and [Using the GitLab Container Registry documentation](../ci/docker/using_docker_build.md#using-the-gitlab-container-registry).
+
+## Limitations
+
+In order to use a container image from your private project as an `image:` in
+your `.gitlab-ci.yml`, you have to follow the
+[Using a private Docker Registry][private-docker]
+documentation. This workflow will be simplified in the future.
+
+## Troubleshooting the GitLab Container Registry
+
+### Basic Troubleshooting
+
+1. Check to make sure that the system clock on your Docker client and GitLab server have
+ been synchronized (e.g. via NTP).
+
+2. If you are using an S3-backed Registry, double check that the IAM
+ permissions and the S3 credentials (including region) are correct. See [the
+ sample IAM policy](https://docs.docker.com/registry/storage-drivers/s3/)
+ for more details.
+
+3. Check the Registry logs (e.g. `/var/log/gitlab/registry/current`) and the GitLab production logs
+ for errors (e.g. `/var/log/gitlab/gitlab-rails/production.log`). You may be able to find clues
+ there.
+
+### Advanced Troubleshooting
+
+>**NOTE:** The following section is only recommended for experts.
+
+Sometimes it's not obvious what is wrong, and you may need to dive deeper into
+the communication between the Docker client and the Registry to find out
+what's wrong. We will use a concrete example in the past to illustrate how to
+diagnose a problem with the S3 setup.
+
+#### Unexpected 403 error during push
+
+A user attempted to enable an S3-backed Registry. The `docker login` step went
+fine. However, when pushing an image, the output showed:
+
+```
+The push refers to a repository [s3-testing.myregistry.com:4567/root/docker-test]
+dc5e59c14160: Pushing [==================================================>] 14.85 kB
+03c20c1a019a: Pushing [==================================================>] 2.048 kB
+a08f14ef632e: Pushing [==================================================>] 2.048 kB
+228950524c88: Pushing 2.048 kB
+6a8ecde4cc03: Pushing [==> ] 9.901 MB/205.7 MB
+5f70bf18a086: Pushing 1.024 kB
+737f40e80b7f: Waiting
+82b57dbc5385: Waiting
+19429b698a22: Waiting
+9436069b92a3: Waiting
+error parsing HTTP 403 response body: unexpected end of JSON input: ""
+```
+
+This error is ambiguous, as it's not clear whether the 403 is coming from the
+GitLab Rails application, the Docker Registry, or something else. In this
+case, since we know that since the login succeeded, we probably need to look
+at the communication between the client and the Registry.
+
+The REST API between the Docker client and Registry is [described
+here](https://docs.docker.com/registry/spec/api/). Normally, one would just
+use Wireshark or tcpdump to capture the traffic and see where things went
+wrong. However, since all communication between Docker clients and servers
+are done over HTTPS, it's a bit difficult to decrypt the traffic quickly even
+if you know the private key. What can we do instead?
+
+One way would be to disable HTTPS by setting up an [insecure
+Registry](https://docs.docker.com/registry/insecure/). This could introduce a
+security hole and is only recommended for local testing. If you have a
+production system and can't or don't want to do this, there is another way:
+use mitmproxy, which stands for Man-in-the-Middle Proxy.
+
+#### mitmproxy
+
+[mitmproxy](https://mitmproxy.org/) allows you to place a proxy between your
+client and server to inspect all traffic. One wrinkle is that your system
+needs to trust the mitmproxy SSL certificates for this to work.
+
+The following installation instructions assume you are running Ubuntu:
+
+1. Install mitmproxy (see http://docs.mitmproxy.org/en/stable/install.html)
+1. Run `mitmproxy --port 9000` to generate its certificates.
+ Enter <kbd>CTRL</kbd>-<kbd>C</kbd> to quit.
+1. Install the certificate from `~/.mitmproxy` to your system:
+
+ ```sh
+ sudo cp ~/.mitmproxy/mitmproxy-ca-cert.pem /usr/local/share/ca-certificates/mitmproxy-ca-cert.crt
+ sudo update-ca-certificates
+ ```
+
+If successful, the output should indicate that a certificate was added:
+
+```sh
+Updating certificates in /etc/ssl/certs... 1 added, 0 removed; done.
+Running hooks in /etc/ca-certificates/update.d....done.
+```
+
+To verify that the certificates are properly installed, run:
+
+```sh
+mitmproxy --port 9000
+```
+
+This will run mitmproxy on port `9000`. In another window, run:
+
+```sh
+curl --proxy http://localhost:9000 https://httpbin.org/status/200
+```
+
+If everything is setup correctly, you will see information on the mitmproxy window and
+no errors from the curl commands.
+
+#### Running the Docker daemon with a proxy
+
+For Docker to connect through a proxy, you must start the Docker daemon with the
+proper environment variables. The easiest way is to shutdown Docker (e.g. `sudo initctl stop docker`)
+and then run Docker by hand. As root, run:
+
+```sh
+export HTTP_PROXY="http://localhost:9000"
+export HTTPS_PROXY="https://localhost:9000"
+docker daemon --debug
+```
+
+This will launch the Docker daemon and proxy all connections through mitmproxy.
+
+#### Running the Docker client
+
+Now that we have mitmproxy and Docker running, we can attempt to login and push
+a container image. You may need to run as root to do this. For example:
+
+```sh
+docker login s3-testing.myregistry.com:4567
+docker push s3-testing.myregistry.com:4567/root/docker-test
+```
+
+In the example above, we see the following trace on the mitmproxy window:
+
+![mitmproxy output from Docker](img/mitmproxy-docker.png)
+
+The above image shows:
+
+* The initial PUT requests went through fine with a 201 status code.
+* The 201 redirected the client to the S3 bucket.
+* The HEAD request to the AWS bucket reported a 403 Unauthorized.
+
+What does this mean? This strongly suggests that the S3 user does not have the right
+[permissions to perform a HEAD request](http://docs.aws.amazon.com/AmazonS3/latest/API/RESTObjectHEAD.html).
+The solution: check the [IAM permissions again](https://docs.docker.com/registry/storage-drivers/s3/).
+Once the right permissions were set, the error will go away.
+
+[ce-4040]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4040
+[docker-docs]: https://docs.docker.com/engine/userguide/intro/
+[private-docker]: https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/configuration/advanced-configuration.md#using-a-private-docker-registry
diff --git a/doc/user/project/cycle_analytics.md b/doc/user/project/cycle_analytics.md
index abef80e7914..c16058165d7 100644
--- a/doc/user/project/cycle_analytics.md
+++ b/doc/user/project/cycle_analytics.md
@@ -6,7 +6,7 @@
This the first iteration of Cycle Analytics, you can follow the following issue
to track the changes that are coming to this feature: [#20975][ce-20975].
-Cycle Analytics measures the time it takes to go from [an idea to production] for
+Cycle Analytics measures the time it takes to go from an [idea to production] for
each project you have. This is achieved by not only indicating the total time it
takes to reach at that point, but the total time is broken down into the
multiple stages an idea has to pass through to be shipped.
@@ -28,9 +28,10 @@ You can see that there are seven stages in total:
(first assignment, any milestone, milestone date or assignee is not required)
- **Plan** (Board)
- Median time from giving an issue a milestone or label until pushing the
- first commit
+ first commit to the branch
- **Code** (IDE)
- - Median time from the first commit until the merge request is created
+ - Median time from the first commit to the branch until the merge request is
+ created
- **Test** (CI)
- Median total test time for all commits/merges
- **Review** (Merge Request/MR)
@@ -40,7 +41,10 @@ You can see that there are seven stages in total:
- Median time from when the merge request got merged until the deploy to
production (production is last stage/environment)
- **Production** (Total)
- - Sum of all the above stages excluding the Test (CI) time
+ - Sum of all the above stages' times excluding the Test (CI) time. To clarify,
+ it's not so much that CI time is "excluded", but rather CI time is already
+ counted in the review stage since CI is done automatically. Most of the
+ other stages are purely sequential, but **Test** is not.
## How the data is measured
@@ -57,25 +61,24 @@ Below you can see in more detail what the various stages of Cycle Analytics mean
| **Stage** | **Description** |
| --------- | --------------- |
| Issue | Measures the median time between creating an issue and taking action to solve it, by either labeling it or adding it to a milestone, whatever comes first. The label will be tracked only if it already has an [Issue Board list][board] created for it. |
-| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the repository. To make this change tracked, the pushed commit needs to contain the [issue closing pattern], for example `Closes #xxx`, where `xxx` is the number of the issue related to this commit. If the commit does not contain the issue closing pattern, it is not considered to the measurement time of the stage. |
-| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request related to that commit. The key to keep the process tracked is include the [issue closing pattern] to the description of the merge request. |
+| Plan | Measures the median time between the action you took for the previous stage, and pushing the first commit to the branch. The very first commit of the branch is the one that triggers the separation between **Plan** and **Code**, and at least one of the commits in the branch needs to contain the related issue number (e.g., `#42`). If none of the commits in the branch mention the related issue number, it is not considered to the measurement time of the stage. |
+| Code | Measures the median time between pushing a first commit (previous stage) and creating a merge request (MR) related to that commit. The key to keep the process tracked is to include the [issue closing pattern] to the description of the merge request (for example, `Closes #xxx`, where `xxx` is the number of the issue related to this merge request). If the issue closing pattern is not present in the merge request description, the MR is not considered to the measurement time of the stage. |
| Test | Measures the median time to run the entire pipeline for that project. It's related to the time GitLab CI takes to run every job for the commits pushed to that merge request defined in the previous stage. It is basically the start->finish time for all pipelines. `master` is not excluded. It does not attempt to track time for any particular stages. |
| Review | Measures the median time taken to review the merge request, between its creation and until it's merged. |
-| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
-| Production| The sum of all time taken to run the entire process, from issue creation to deploying the code to production. |
+| Staging | Measures the median time between merging the merge request until the very first deployment to production. It's tracked by the [environment] set to `production` (case-sensitive, `Production` won't work) in your GitLab CI configuration. If there isn't a `production` environment, this is not tracked. |
+| Production| The sum of all time (medians) taken to run the entire process, from issue creation to deploying the code to production. |
---
Here's a little explanation of how this works behind the scenes:
1. Issues and merge requests are grouped together in pairs, such that for each
- `<issue, merge request>` pair, the merge request has `Fixes #xxx` for the
- corresponding issue. All other issues and merge requests are **not** considered.
-
+ `<issue, merge request>` pair, the merge request has the [issue closing pattern]
+ for the corresponding issue. All other issues and merge requests are **not**
+ considered.
1. Then the <issue, merge request> pairs are filtered out. Any merge request
that has **not** been deployed to production in the last XX days (specified
by the UI - default is 90 days) prohibits these pairs from being considered.
-
1. For the remaining `<issue, merge request>` pairs, we check the information that
we need for the stages, like issue creation date, merge request merge time,
etc.
@@ -86,6 +89,60 @@ label present in the Issue Board or assigned a milestone or a project has no
`production` environment, the Cycle Analytics dashboard won't present any data
at all.
+## Example workflow
+
+Below is a simple fictional workflow of a single cycle that happens in a
+single day passing through all seven stages. Note that if a stage does not have
+a start/stop mark, it is not measured and hence not calculated in the median
+time. It is assumed that milestones are created and CI for testing and setting
+environments is configured.
+
+1. Issue is created at 09:00 (start of **Issue** stage).
+1. Issue is added to a milestone at 11:00 (stop of **Issue** stage / start of
+ **Plan** stage).
+1. Start working on the issue, create a branch locally and make one commit at
+ 12:00.
+1. Make a second commit to the branch which mentions the issue number at 12.30
+ (stop of **Plan** stage / start of **Code** stage).
+1. Push branch and create a merge request that contains the [issue closing pattern]
+ in its description at 14:00 (stop of **Code** stage / start of **Test** and
+ **Review** stages).
+1. The CI starts running your scripts defined in [`.gitlab-ci.yml`][yml] and
+ takes 5min (stop of **Test** stage).
+1. Review merge request, ensure that everything is OK and merge the merge
+ request at 19:00. (stop of **Review** stage / start of **Staging** stage).
+1. Now that the merge request is merged, a deployment to the `production`
+ environment starts and finishes at 19:30 (stop of **Staging** stage).
+1. The cycle completes and the sum of the median times of the previous stages
+ is recorded to the **Production** stage. That is the time between creating an
+ issue and deploying its relevant merge request to production.
+
+From the above example you can conclude the time it took each stage to complete
+as long as their total time:
+
+- **Issue**: 2h (11:00 - 09:00)
+- **Plan**: 1h (12:00 - 11:00)
+- **Code**: 2h (14:00 - 12:00)
+- **Test**: 5min
+- **Review**: 5h (19:00 - 14:00)
+- **Staging**: 30min (19:30 - 19:00)
+- **Production**: Since this stage measures the sum of median time off all
+ previous stages, we cannot calculate it if we don't know the status of the
+ stages before. In case this is the very first cycle that is run in the project,
+ then the **Production** time is 10h 30min (19:30 - 09:00)
+
+A few notes:
+
+- In the above example we demonstrated that it doesn't matter if your first
+ commit doesn't mention the issue number, you can do this later in any commit
+ of the branch you are working on.
+- You can see that the **Test** stage is not calculated to the overall time of
+ the cycle since it is included in the **Review** process (every MR should be
+ tested).
+- The example above was just **one cycle** of the seven stages. Add multiple
+ cycles, calculate their median time and the result is what the dashboard of
+ Cycle Analytics is showing.
+
## Permissions
The current permissions on the Cycle Analytics dashboard are:
@@ -104,11 +161,12 @@ Learn more about Cycle Analytics in the following resources:
- [Cycle Analytics feature highlight](https://about.gitlab.com/2016/09/21/cycle-analytics-feature-highlight/)
+[board]: issue_board.md#creating-a-new-list
[ce-5986]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5986
[ce-20975]: https://gitlab.com/gitlab-org/gitlab-ce/issues/20975
-[GitLab flow]: ../../workflow/gitlab_flow.md
-[permissions]: ../permissions.md
[environment]: ../../ci/yaml/README.md#environment
-[board]: issue_board.md#creating-a-new-list
+[GitLab flow]: ../../workflow/gitlab_flow.md
[idea to production]: https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/#from-idea-to-production-with-gitlab
[issue closing pattern]: issues/automatic_issue_closing.md
+[permissions]: ../permissions.md
+[yml]: ../../ci/yaml/README.md
diff --git a/doc/user/project/img/container_registry_enable.png b/doc/user/project/img/container_registry_enable.png
new file mode 100644
index 00000000000..6fffa2a91d8
--- /dev/null
+++ b/doc/user/project/img/container_registry_enable.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
new file mode 100644
index 00000000000..60fd76192b7
--- /dev/null
+++ b/doc/user/project/img/container_registry_panel.png
Binary files differ
diff --git a/doc/user/project/img/container_registry_tab.png b/doc/user/project/img/container_registry_tab.png
new file mode 100644
index 00000000000..36b883aaa97
--- /dev/null
+++ b/doc/user/project/img/container_registry_tab.png
Binary files differ
diff --git a/doc/user/project/img/cycle_analytics_landing_page.png b/doc/user/project/img/cycle_analytics_landing_page.png
index 4fa42c87395..b212134d5ed 100644
--- a/doc/user/project/img/cycle_analytics_landing_page.png
+++ b/doc/user/project/img/cycle_analytics_landing_page.png
Binary files differ
diff --git a/doc/container_registry/img/mitmproxy-docker.png b/doc/user/project/img/mitmproxy-docker.png
index 4e3e37b413d..4e3e37b413d 100644
--- a/doc/container_registry/img/mitmproxy-docker.png
+++ b/doc/user/project/img/mitmproxy-docker.png
Binary files differ
diff --git a/doc/user/project/issue_board.md b/doc/user/project/issue_board.md
index cac926b3e28..4a6c0d88241 100644
--- a/doc/user/project/issue_board.md
+++ b/doc/user/project/issue_board.md
@@ -31,9 +31,10 @@ Below is a table of the definitions used for GitLab's Issue Board.
There are three types of lists, the ones you create based on your labels, and
two default:
-- **Backlog** (default): shows all opened issues that do not fall in one of the other lists. Always appears on the very left.
-- **Done** (default): shows all closed issues that do not fall in one of the other lists. Always appears on the very right.
-- Label list: a list based on a label. It shows all opened or closed issues with that label.
+- **Backlog** (default): shows all issues that do not fall in one of the other lists. Always appears on the very left.
+- **Done** (default): shows all closed issues. Always appears on the very right.
+Label list: a list based on a label. It shows all issues with that label.
+- Label list: a list based on a label. It shows all opened issues with that label.
![GitLab Issue Board](img/issue_board.png)
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index e73f60023b5..5253825d507 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -98,6 +98,9 @@ As an Administrator, you can verify that the user is a member of the group or
project they're trying to have access to, and you can impersonate the user to
retry the failing build in order to verify that everything is correct.
+You need to make sure that your installation has HTTPS cloning enabled.
+HTTPS support is required by GitLab CI to clone all sources.
+
## Build triggers
[Build triggers][triggers] do not support the new permission model.
diff --git a/doc/user/project/repository/img/web_editor_new_branch_from_issue.png b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
new file mode 100644
index 00000000000..b0a63ddf0ab
--- /dev/null
+++ b/doc/user/project/repository/img/web_editor_new_branch_from_issue.png
Binary files differ
diff --git a/doc/user/project/repository/web_editor.md b/doc/user/project/repository/web_editor.md
index 993c6bfb7e9..675e89e4247 100644
--- a/doc/user/project/repository/web_editor.md
+++ b/doc/user/project/repository/web_editor.md
@@ -97,11 +97,11 @@ There are multiple ways to create a branch from GitLab's web interface.
In case your development workflow dictates to have an issue for every merge
request, you can quickly create a branch right on the issue page which will be
-tied with the issue itself. You can see a **New Branch** button after the issue
+tied with the issue itself. You can see a **New branch** button after the issue
description, unless there is already a branch with the same name or a referenced
merge request.
-![New Branch Button](img/new_branch_from_issue.png)
+![New Branch Button](img/web_editor_new_branch_from_issue.png)
Once you click it, a new branch will be created that diverges from the default
branch of your project, by default `master`. The branch name will be based on
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
index 445c0ee8333..65ed9fae4ec 100644
--- a/doc/user/project/settings/import_export.md
+++ b/doc/user/project/settings/import_export.md
@@ -7,7 +7,8 @@
> that of the exporter.
> - For existing installations, the project import option has to be enabled in
> application settings (`/admin/application_settings`) under 'Import sources'.
-> You will have to be an administrator to enable and use the import functionality.
+> Ask your administrator if you don't see the **GitLab export** button when
+> creating a new project.
> - You can find some useful raketasks if you are an administrator in the
> [import_export](../../../administration/raketasks/project_import_export.md)
> raketask.
diff --git a/doc/user/project/slash_commands.md b/doc/user/project/slash_commands.md
index 1792a0c501d..5f6a6c6503e 100644
--- a/doc/user/project/slash_commands.md
+++ b/doc/user/project/slash_commands.md
@@ -27,4 +27,5 @@ do.
| `/subscribe` | Subscribe |
| `/unsubscribe` | Unsubscribe |
| <code>/due &lt;in 2 days &#124; this Friday &#124; December 31st&gt;</code> | Set due date |
-| `/remove_due_date` | Remove due date |
+| `/remove_due_date` | Remove due date |
+| `/wip` | Toggle the Work In Progress status |
diff --git a/doc/workflow/importing/migrating_from_svn.md b/doc/workflow/importing/migrating_from_svn.md
index 4828bb5dce6..423b095e69e 100644
--- a/doc/workflow/importing/migrating_from_svn.md
+++ b/doc/workflow/importing/migrating_from_svn.md
@@ -4,6 +4,112 @@ Subversion (SVN) is a central version control system (VCS) while
Git is a distributed version control system. There are some major differences
between the two, for more information consult your favorite search engine.
+## Overview
+
+There are two approaches to SVN to Git migration:
+
+1. [Git/SVN Mirror](#smooth-migration-with-a-gitsvn-mirror-using-subgit) which:
+ - Makes the GitLab repository to mirror the SVN project.
+ - Git and SVN repositories are kept in sync; you can use either one.
+ - Smoothens the migration process and allows to manage migration risks.
+
+1. [Cut over migration](#cut-over-migration-with-svn2git) which:
+ - Translates and imports the existing data and history from SVN to Git.
+ - Is a fire and forget approach, good for smaller teams.
+
+## Smooth migration with a Git/SVN mirror using SubGit
+
+[SubGit](https://subgit.com) is a tool for a smooth, stress-free SVN to Git
+migration. It creates a writable Git mirror of a local or remote Subversion
+repository and that way you can use both Subversion and Git as long as you like.
+It requires access to your GitLab server as it talks with the Git repositories
+directly in a filesystem level.
+
+### SubGit prerequisites
+
+1. Install Oracle JRE 1.8 or newer. On Debian-based Linux distributions you can
+ follow [this article](http://www.webupd8.org/2012/09/install-oracle-java-8-in-ubuntu-via-ppa.html).
+1. Download SubGit from https://subgit.com/download/.
+1. Unpack the downloaded SubGit zip archive to the `/opt` directory. The `subgit`
+ command will be available at `/opt/subgit-VERSION/bin/subgit`.
+
+### SubGit configuration
+
+The first step to mirror you SVN repository in GitLab is to create a new empty
+project which will be used as a mirror. For Omnibus installations the path to
+the repository will be located at
+`/var/opt/gitlab/git-data/repositories/USER/REPO.git` by default. For
+installations from source, the default repository directory will be
+`/home/git/repositories/USER/REPO.git`. For convenience, assign this path to a
+variable:
+
+```
+GIT_REPO_PATH=/var/opt/gitlab/git-data/repositories/USER/REPOS.git
+```
+
+SubGit will keep this repository in sync with a remote SVN project. For
+convenience, assign your remote SVN project URL to a variable:
+
+```
+SVN_PROJECT_URL=http://svn.company.com/repos/project
+```
+
+Next you need to run SubGit to set up a Git/SVN mirror. Make sure the following
+`subgit` command is ran on behalf of the same user that keeps ownership of
+GitLab Git repositories (by default `git`):
+
+```
+subgit configure --layout auto $SVN_PROJECT_URL $GIT_REPO_PATH
+```
+
+Adjust authors and branches mappings, if necessary. Open with your favorite
+text editor:
+
+```
+edit $GIT_REPO_PATH/subgit/authors.txt
+edit $GIT_REPO_PATH/subgit/config
+```
+
+For more information regarding the SubGit configuration options, refer to
+[SubGit's documentation](https://subgit.com/documentation.html) website.
+
+### Initial translation
+
+Now that SubGit has configured the Git/SVN repos, run `subgit` to perform the
+initial translation of existing SVN revisions into the Git repository:
+
+```
+subgit install $GIT_REPOS_PATH
+```
+
+After the initial translation is completed, the Git repository and the SVN
+project will be kept in sync by `subgit` - new Git commits will be translated to
+SVN revisions and new SVN revisions will be translated to Git commits. Mirror
+works transparently and does not require any special commands.
+
+If you would prefer to perform one-time cut over migration with `subgit`, use
+the `import` command instead of `install`:
+
+```
+subgit import $GIT_REPO_PATH
+```
+
+### SubGit licensing
+
+Running SubGit in a mirror mode requires a
+[registration](https://subgit.com/pricing.html). Registration is free for open
+source, academic and startup projects.
+
+We're currently working on deeper GitLab/SubGit integration. You may track our
+progress at [this issue](https://gitlab.com/gitlab-org/gitlab-ee/issues/990).
+
+### SubGit support
+
+For any questions related to SVN to GitLab migration with SubGit, you can
+contact the SubGit team directly at [support@subgit.com](mailto:support@subgit.com).
+
+## Cut over migration with svn2git
+
If you are currently using an SVN repository, you can migrate the repository
to Git and GitLab. We recommend a hard cut over - run the migration command once
and then have all developers start using the new GitLab repository immediately.
@@ -75,5 +181,3 @@ git push --tags origin
## Contribute to this guide
We welcome all contributions that would expand this guide with instructions on
how to migrate from SVN and other version control systems.
-
-
diff --git a/features/profile/ssh_keys.feature b/features/profile/ssh_keys.feature
deleted file mode 100644
index b0d5b748916..00000000000
--- a/features/profile/ssh_keys.feature
+++ /dev/null
@@ -1,20 +0,0 @@
-@profile
-Feature: Profile SSH Keys
- Background:
- Given I sign in as a user
- And I have ssh key "ssh-rsa Work"
- And I visit profile keys page
-
- Scenario: I should see ssh keys
- Then I should see my ssh keys
-
- Scenario: Add new ssh key
- Given I should see new ssh key form
- And I submit new ssh key "Laptop"
- Then I should see new ssh key "Laptop"
-
- Scenario: Remove ssh key
- Given I click link "Work"
- And I click link "Remove"
- Then I visit profile keys page
- And I should not see "Work" ssh key
diff --git a/features/project/commits/commits.feature b/features/project/commits/commits.feature
index 8b0cb90765e..1776c07e60e 100644
--- a/features/project/commits/commits.feature
+++ b/features/project/commits/commits.feature
@@ -37,6 +37,11 @@ Feature: Project Commits
Then I see commit info
And I see side-by-side diff button
+ Scenario: I browse commit from list and create a new tag
+ Given I click on commit link
+ And I click on tag link
+ Then I see commit SHA pre-filled
+
Scenario: I browse commit with ci from list
Given commit has ci status
And repository contains ".gitlab-ci.yml" file
diff --git a/features/project/issues/issues.feature b/features/project/issues/issues.feature
index 358e622b736..80670063ea0 100644
--- a/features/project/issues/issues.feature
+++ b/features/project/issues/issues.feature
@@ -37,6 +37,7 @@ Feature: Project Issues
And I submit new issue "500 error on profile"
Then I should see issue "500 error on profile"
+ @javascript
Scenario: I submit new unassigned issue with labels
Given project "Shop" has labels: "bug", "feature", "enhancement"
And I click link "New Issue"
diff --git a/features/project/snippets.feature b/features/project/snippets.feature
index 270557cbde7..3c51ea56585 100644
--- a/features/project/snippets.feature
+++ b/features/project/snippets.feature
@@ -12,7 +12,7 @@ Feature: Project Snippets
And I should not see "Snippet two" in snippets
Scenario: I create new project snippet
- Given I click link "New Snippet"
+ Given I click link "New snippet"
And I submit new snippet "Snippet three"
Then I should see snippet "Snippet three"
diff --git a/features/project/source/browse_files.feature b/features/project/source/browse_files.feature
index fdffd71de85..d4b91fec6e8 100644
--- a/features/project/source/browse_files.feature
+++ b/features/project/source/browse_files.feature
@@ -71,6 +71,7 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
+ When I click on "Changes" tab
And I should see its new content
@javascript
@@ -80,9 +81,10 @@ Feature: Project Source Browse Files
And I fill the upload file commit message
And I fill the new branch name
And I click on "Upload file"
- Then I can see the new text file
+ Then I can see the new commit message
And I am redirected to the new merge request page
- And I can see the new commit message
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: I can upload file and commit when I don't have write access
@@ -93,9 +95,10 @@ Feature: Project Source Browse Files
And I upload a new text file
And I fill the upload file commit message
And I click on "Upload file"
- Then I can see the new text file
+ Then I can see the new commit message
And I am redirected to the fork's new merge request page
- And I can see the new commit message
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: I can replace file and commit
@@ -119,9 +122,10 @@ Feature: Project Source Browse Files
And I replace it with a text file
And I fill the replace file commit message
And I click on "Replace file"
- Then I can see the new text file
- And I am redirected to the fork's new merge request page
And I can see the replacement commit message
+ And I am redirected to the fork's new merge request page
+ When I click on "Changes" tab
+ Then I can see the new text file
@javascript
Scenario: If I enter an illegal file name I see an error message
@@ -191,6 +195,7 @@ Feature: Project Source Browse Files
And I fill the new branch name
And I click on "Commit Changes"
Then I am redirected to the new merge request page
+ Then I click on "Changes" tab
And I should see its new content
@javascript @wip
diff --git a/features/steps/dashboard/new_project.rb b/features/steps/dashboard/new_project.rb
index 2f0941e4113..cb36d6ae1a9 100644
--- a/features/steps/dashboard/new_project.rb
+++ b/features/steps/dashboard/new_project.rb
@@ -20,6 +20,7 @@ class Spinach::Features::NewProject < Spinach::FeatureSteps
expect(page).to have_link('GitLab.com')
expect(page).to have_link('Google Code')
expect(page).to have_link('Repo by URL')
+ expect(page).to have_link('GitLab export')
end
step 'I click on "Import project from GitHub"' do
diff --git a/features/steps/profile/profile.rb b/features/steps/profile/profile.rb
index 4ee6784a086..05ab2a7dc73 100644
--- a/features/steps/profile/profile.rb
+++ b/features/steps/profile/profile.rb
@@ -13,6 +13,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
fill_in 'user_website_url', with: 'testurl'
fill_in 'user_location', with: 'Ukraine'
fill_in 'user_bio', with: 'I <3 GitLab'
+ fill_in 'user_organization', with: 'GitLab'
click_button 'Update profile settings'
@user.reload
end
@@ -23,6 +24,7 @@ class Spinach::Features::Profile < Spinach::FeatureSteps
expect(@user.twitter).to eq 'testtwitter'
expect(@user.website_url).to eq 'testurl'
expect(@user.bio).to eq 'I <3 GitLab'
+ expect(@user.organization).to eq 'GitLab'
expect(find('#user_location').value).to eq 'Ukraine'
end
diff --git a/features/steps/profile/ssh_keys.rb b/features/steps/profile/ssh_keys.rb
deleted file mode 100644
index a400488a532..00000000000
--- a/features/steps/profile/ssh_keys.rb
+++ /dev/null
@@ -1,46 +0,0 @@
-class Spinach::Features::ProfileSshKeys < Spinach::FeatureSteps
- include SharedAuthentication
-
- step 'I should see my ssh keys' do
- @user.keys.each do |key|
- expect(page).to have_content(key.title)
- end
- end
-
- step 'I should see new ssh key form' do
- expect(page).to have_content("Add an SSH key")
- end
-
- step 'I submit new ssh key "Laptop"' do
- fill_in "key_title", with: "Laptop"
- fill_in "key_key", with: "ssh-rsa AAAAB3NzaC1yc2EAAAABIwAAAQEAzrEJUIR6Y03TCE9rIJ+GqTBvgb8t1jI9h5UBzCLuK4VawOmkLornPqLDrGbm6tcwM/wBrrLvVOqi2HwmkKEIecVO0a64A4rIYScVsXIniHRS6w5twyn1MD3sIbN+socBDcaldECQa2u1dI3tnNVcs8wi77fiRe7RSxePsJceGoheRQgC8AZ510UdIlO+9rjIHUdVN7LLyz512auAfYsgx1OfablkQ/XJcdEwDNgi9imI6nAXhmoKUm1IPLT2yKajTIC64AjLOnE0YyCh6+7RFMpiMyu1qiOCpdjYwTgBRiciNRZCH8xIedyCoAmiUgkUT40XYHwLuwiPJICpkAzp7Q== user@laptop"
- click_button "Add key"
- end
-
- step 'I should see new ssh key "Laptop"' do
- key = Key.find_by(title: "Laptop")
- expect(page).to have_content(key.title)
- expect(page).to have_content(key.key)
- expect(current_path).to eq profile_key_path(key)
- end
-
- step 'I click link "Work"' do
- click_link "Work"
- end
-
- step 'I click link "Remove"' do
- click_link "Remove"
- end
-
- step 'I visit profile keys page' do
- visit profile_keys_path
- end
-
- step 'I should not see "Work" ssh key' do
- expect(page).not_to have_content "Work"
- end
-
- step 'I have ssh key "ssh-rsa Work"' do
- create(:key, user: @user, title: "ssh-rsa Work", key: "ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQC+L3TbFegm3k8QjejSwemk4HhlRh+DuN679Pc5ckqE/MPhVtE/+kZQDYCTB284GiT2aIoGzmZ8ee9TkaoejAsBwlA+Wz2Q3vhz65X6sMgalRwpdJx8kSEUYV8ZPV3MZvPo8KdNg993o4jL6G36GDW4BPIyO6FPZhfsawdf6liVD0Xo5kibIK7B9VoE178cdLQtLpS2YolRwf5yy6XR6hbbBGQR+6xrGOdP16eGZDb1CE2bMvvJijjloFqPscGktWOqW+nfh5txwFfBzlfARDTBsS8WZtg3Yoj1kn33kPsWRlgHfNutFRAIynDuDdQzQq8tTtVwm+Yi75RfcPHW8y3P Work")
- end
-end
diff --git a/features/steps/project/commits/commits.rb b/features/steps/project/commits/commits.rb
index bea9f9d198b..b8264f97687 100644
--- a/features/steps/project/commits/commits.rb
+++ b/features/steps/project/commits/commits.rb
@@ -24,6 +24,14 @@ class Spinach::Features::ProjectCommits < Spinach::FeatureSteps
expect(body).to have_selector("entry summary", text: commit.description[0..10])
end
+ step 'I click on tag link' do
+ click_link "Tag"
+ end
+
+ step 'I see commit SHA pre-filled' do
+ expect(page).to have_selector("input[value='#{sample_commit.id}']")
+ end
+
step 'I click on commit link' do
visit namespace_project_commit_path(@project.namespace, @project, sample_commit.id)
end
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 8abeb5ee242..70dbd030003 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -70,6 +70,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
step 'There is an existent fork of the "Shop" project' do
user = create(:user, name: 'Mike')
+ @project.team << [user, :reporter]
@forked_project = Projects::ForkService.new(@project, user).execute
end
diff --git a/features/steps/project/forked_merge_requests.rb b/features/steps/project/forked_merge_requests.rb
index dacab6c7977..6c14d835004 100644
--- a/features/steps/project/forked_merge_requests.rb
+++ b/features/steps/project/forked_merge_requests.rb
@@ -138,19 +138,19 @@ class Spinach::Features::ProjectForkedMergeRequests < Spinach::FeatureSteps
end
step 'I click "Assign to" dropdown"' do
- first('.ajax-users-select').click
+ click_button 'Assignee'
end
step 'I should see the target project ID in the input selector' do
- expect(page).to have_selector("input[data-project-id=\"#{@project.id}\"]")
+ expect(find('.js-assignee-search')["data-project-id"]).to eq "#{@project.id}"
end
step 'I should see the users from the target project ID' do
- expect(page).to have_selector('.user-result', visible: true, count: 3)
- users = page.all('.user-name')
- expect(users[0].text).to eq 'Unassigned'
- expect(users[1].text).to eq current_user.name
- expect(users[2].text).to eq @project.users.first.name
+ page.within '.dropdown-menu-user' do
+ expect(page).to have_content 'Unassigned'
+ expect(page).to have_content current_user.name
+ expect(page).to have_content @project.users.first.name
+ end
end
# Verify a link is generated against the correct project
diff --git a/features/steps/project/issues/issues.rb b/features/steps/project/issues/issues.rb
index ed7241679ee..b50f5238e80 100644
--- a/features/steps/project/issues/issues.rb
+++ b/features/steps/project/issues/issues.rb
@@ -84,7 +84,8 @@ class Spinach::Features::ProjectIssues < Spinach::FeatureSteps
step 'I submit new issue "500 error on profile" with label \'bug\'' do
fill_in "issue_title", with: "500 error on profile"
- select 'bug', from: "Labels"
+ click_button "Label"
+ click_link "bug"
click_button "Submit issue"
end
diff --git a/features/steps/project/snippets.rb b/features/steps/project/snippets.rb
index beb8ecfc799..5e7d539add6 100644
--- a/features/steps/project/snippets.rb
+++ b/features/steps/project/snippets.rb
@@ -21,8 +21,8 @@ class Spinach::Features::ProjectSnippets < Spinach::FeatureSteps
author: project.users.first)
end
- step 'I click link "New Snippet"' do
- click_link "New Snippet"
+ step 'I click link "New snippet"' do
+ click_link "New snippet"
end
step 'I click link "Snippet one"' do
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index bb79424ee08..1cc9e37b075 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -105,6 +105,10 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
click_button 'Commit Changes'
end
+ step 'I click on "Changes" tab' do
+ click_link 'Changes'
+ end
+
step 'I click on "Create directory"' do
click_button 'Create directory'
end
diff --git a/lib/api/access_requests.rb b/lib/api/access_requests.rb
index 29a97ccbd75..87915b19480 100644
--- a/lib/api/access_requests.rb
+++ b/lib/api/access_requests.rb
@@ -5,32 +5,27 @@ module API
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
resource source_type.pluralize do
- # Get a list of group/project access requests viewable by the authenticated user.
- #
- # Parameters:
- # id (required) - The group/project ID
- #
- # Example Request:
- # GET /groups/:id/access_requests
- # GET /projects/:id/access_requests
+ desc "Gets a list of access requests for a #{source_type}." do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::AccessRequester
+ end
get ":id/access_requests" do
source = find_source(source_type, params[:id])
- authorize_admin_source!(source_type, source)
- access_requesters = paginate(source.requesters.includes(:user))
+ access_requesters = AccessRequestsFinder.new(source).execute!(current_user)
+ access_requesters = paginate(access_requesters.includes(:user))
present access_requesters.map(&:user), with: Entities::AccessRequester, source: source
end
- # Request access to the group/project
- #
- # Parameters:
- # id (required) - The group/project ID
- #
- # Example Request:
- # POST /groups/:id/access_requests
- # POST /projects/:id/access_requests
+ desc "Requests access for the authenticated user to a #{source_type}." do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::AccessRequester
+ end
post ":id/access_requests" do
source = find_source(source_type, params[:id])
access_requester = source.request_access(current_user)
@@ -42,47 +37,34 @@ module API
end
end
- # Approve a group/project access request
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the access requester
- # access_level (optional) - Access level
- #
- # Example Request:
- # PUT /groups/:id/access_requests/:user_id/approve
- # PUT /projects/:id/access_requests/:user_id/approve
+ desc 'Approves an access request for the given user.' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+ optional :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ end
put ':id/access_requests/:user_id/approve' do
- required_attributes! [:user_id]
source = find_source(source_type, params[:id])
- authorize_admin_source!(source_type, source)
- member = source.requesters.find_by!(user_id: params[:user_id])
- if params[:access_level]
- member.update(access_level: params[:access_level])
- end
- member.accept_request
+ member = ::Members::ApproveAccessRequestService.new(source, current_user, declared(params)).execute
status :created
present member.user, with: Entities::Member, member: member
end
- # Deny a group/project access request
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the access requester
- #
- # Example Request:
- # DELETE /groups/:id/access_requests/:user_id
- # DELETE /projects/:id/access_requests/:user_id
+ desc 'Denies an access request for the given user.' do
+ detail 'This feature was introduced in GitLab 8.11.'
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the access requester'
+ end
delete ":id/access_requests/:user_id" do
- required_attributes! [:user_id]
source = find_source(source_type, params[:id])
- access_requester = source.requesters.find_by!(user_id: params[:user_id])
-
- ::Members::DestroyService.new(access_requester, current_user).execute
+ ::Members::DestroyService.new(source, current_user, params).
+ execute(:requesters)
end
end
end
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 74ca4728695..99722a0a65c 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -28,6 +28,7 @@ module API
helpers ::SentryHelper
helpers ::API::Helpers
+ # Keep in alphabetical order
mount ::API::AccessRequests
mount ::API::AwardEmoji
mount ::API::Branches
@@ -42,12 +43,14 @@ module API
mount ::API::Groups
mount ::API::Internal
mount ::API::Issues
+ mount ::API::Boards
mount ::API::Keys
mount ::API::Labels
mount ::API::LicenseTemplates
mount ::API::Lint
mount ::API::Members
mount ::API::MergeRequests
+ mount ::API::MergeRequestDiffs
mount ::API::Milestones
mount ::API::Namespaces
mount ::API::Notes
@@ -70,6 +73,9 @@ module API
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
- mount ::API::MergeRequestDiffs
+
+ route :any, '*path' do
+ error!('404 Not Found', 404)
+ end
end
end
diff --git a/lib/api/award_emoji.rb b/lib/api/award_emoji.rb
index 2461a783ea8..e9ccba3b465 100644
--- a/lib/api/award_emoji.rb
+++ b/lib/api/award_emoji.rb
@@ -8,16 +8,19 @@ module API
awardable_string = awardable_type.pluralize
awardable_id_string = "#{awardable_type}_id"
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ requires :"#{awardable_id_string}", type: Integer, desc: "The ID of an Issue, Merge Request or Snippet"
+ end
+
[ ":id/#{awardable_string}/:#{awardable_id_string}/award_emoji",
":id/#{awardable_string}/:#{awardable_id_string}/notes/:note_id/award_emoji"
].each do |endpoint|
- # Get a list of project +awardable+ award emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or MR
- # Example Request:
- # GET /projects/:id/issues/:awardable_id/award_emoji
+
+ desc 'Get a list of project +awardable+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
get endpoint do
if can_read_awardable?
awards = paginate(awardable.award_emoji)
@@ -27,14 +30,13 @@ module API
end
end
- # Get a specific award emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or MR
- # award_id (required) - The ID of the award
- # Example Request:
- # GET /projects/:id/issues/:awardable_id/award_emoji/:award_id
+ desc 'Get a specific award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of the award'
+ end
get "#{endpoint}/:award_id" do
if can_read_awardable?
present awardable.award_emoji.find(params[:award_id]), with: Entities::AwardEmoji
@@ -43,17 +45,14 @@ module API
end
end
- # Award a new Emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or mr
- # name (required) - The name of a award_emoji (without colons)
- # Example Request:
- # POST /projects/:id/issues/:awardable_id/award_emoji
+ desc 'Award a new Emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :name, type: String, desc: 'The name of a award_emoji (without colons)'
+ end
post endpoint do
- required_attributes! [:name]
-
not_found!('Award Emoji') unless can_read_awardable? && can_award_awardable?
award = awardable.create_award_emoji(params[:name], current_user)
@@ -65,14 +64,13 @@ module API
end
end
- # Delete a +awardables+ award emoji
- #
- # Parameters:
- # id (required) - The ID of a project
- # awardable_id (required) - The ID of an issue or MR
- # award_emoji_id (required) - The ID of an award emoji
- # Example Request:
- # DELETE /projects/:id/issues/:issue_id/notes/:note_id/award_emoji/:award_id
+ desc 'Delete a +awardables+ award emoji' do
+ detail 'This feature was introduced in 8.9'
+ success Entities::AwardEmoji
+ end
+ params do
+ requires :award_id, type: Integer, desc: 'The ID of an award emoji'
+ end
delete "#{endpoint}/:award_id" do
award = awardable.award_emoji.find(params[:award_id])
diff --git a/lib/api/boards.rb b/lib/api/boards.rb
new file mode 100644
index 00000000000..4d5d144a02e
--- /dev/null
+++ b/lib/api/boards.rb
@@ -0,0 +1,115 @@
+module API
+ # Boards API
+ class Boards < Grape::API
+ before { authenticate! }
+
+ resource :projects do
+ # Get the project board
+ get ':id/boards' do
+ authorize!(:read_board, user_project)
+ present [user_project.board], with: Entities::Board
+ end
+
+ segment ':id/boards/:board_id' do
+ helpers do
+ def project_board
+ board = user_project.board
+ if params[:board_id].to_i == board.id
+ board
+ else
+ not_found!('Board')
+ end
+ end
+
+ def board_lists
+ project_board.lists.destroyable
+ end
+ end
+
+ # Get the lists of a project board
+ # Does not include `backlog` and `done` lists
+ get '/lists' do
+ authorize!(:read_board, user_project)
+ present board_lists, with: Entities::List
+ end
+
+ # Get a list of a project board
+ get '/lists/:list_id' do
+ authorize!(:read_board, user_project)
+ present board_lists.find(params[:list_id]), with: Entities::List
+ end
+
+ # Create a new board list
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # label_id (required) - The ID of an existing label
+ # Example Request:
+ # POST /projects/:id/boards/:board_id/lists
+ post '/lists' do
+ required_attributes! [:label_id]
+
+ unless user_project.labels.exists?(params[:label_id])
+ render_api_error!({ error: "Label not found!" }, 400)
+ end
+
+ authorize!(:admin_list, user_project)
+
+ list = ::Boards::Lists::CreateService.new(user_project, current_user,
+ { label_id: params[:label_id] }).execute
+
+ if list.valid?
+ present list, with: Entities::List
+ else
+ render_validation_error!(list)
+ end
+ end
+
+ # Moves a board list to a new position
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # board_id (required) - The ID of a board
+ # position (required) - The position of the list
+ # Example Request:
+ # PUT /projects/:id/boards/:board_id/lists/:list_id
+ put '/lists/:list_id' do
+ list = project_board.lists.movable.find(params[:list_id])
+
+ authorize!(:admin_list, user_project)
+
+ moved = ::Boards::Lists::MoveService.new(user_project, current_user,
+ { position: params[:position].to_i }).execute(list)
+
+ if moved
+ present list, with: Entities::List
+ else
+ render_api_error!({ error: "List could not be moved!" }, 400)
+ end
+ end
+
+ # Delete a board list
+ #
+ # Parameters:
+ # id (required) - The ID of a project
+ # board_id (required) - The ID of a board
+ # list_id (required) - The ID of a board list
+ # Example Request:
+ # DELETE /projects/:id/boards/:board_id/lists/:list_id
+ delete "/lists/:list_id" do
+ list = board_lists.find_by(id: params[:list_id])
+
+ authorize!(:admin_list, user_project)
+
+ if list
+ destroyed_list = ::Boards::Lists::DestroyService.new(
+ user_project, current_user).execute(list)
+ present destroyed_list, with: Entities::List
+ else
+ not_found!('List')
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index b4eaf1813d4..14ddc8c9a62 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -29,6 +29,42 @@ module API
present commits, with: Entities::RepoCommit
end
+ desc 'Commit multiple file changes as one commit' do
+ detail 'This feature was introduced in GitLab 8.13'
+ end
+
+ params do
+ requires :id, type: Integer, desc: 'The project ID'
+ requires :branch_name, type: String, desc: 'The name of branch'
+ requires :commit_message, type: String, desc: 'Commit message'
+ requires :actions, type: Array, desc: 'Actions to perform in commit'
+ optional :author_email, type: String, desc: 'Author email for commit'
+ optional :author_name, type: String, desc: 'Author name for commit'
+ end
+
+ post ":id/repository/commits" do
+ authorize! :push_code, user_project
+
+ attrs = declared(params)
+ attrs[:source_branch] = attrs[:branch_name]
+ attrs[:target_branch] = attrs[:branch_name]
+ attrs[:actions].map! do |action|
+ action[:action] = action[:action].to_sym
+ action[:file_path].slice!(0) if action[:file_path] && action[:file_path].start_with?('/')
+ action[:previous_path].slice!(0) if action[:previous_path] && action[:previous_path].start_with?('/')
+ action
+ end
+
+ result = ::Files::MultiService.new(user_project, current_user, attrs).execute
+
+ if result[:status] == :success
+ commit_detail = user_project.repository.commits(result[:result], limit: 1).first
+ present commit_detail, with: Entities::RepoCommitDetail
+ else
+ render_api_error!(result[:message], 400)
+ end
+ end
+
# Get a specific commit of a project
#
# Parameters:
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 92a6f29adb0..feaa0c213bf 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -15,7 +15,7 @@ module API
class User < UserBasic
expose :created_at
expose :is_admin?, as: :is_admin
- expose :bio, :location, :skype, :linkedin, :twitter, :website_url
+ expose :bio, :location, :skype, :linkedin, :twitter, :website_url, :organization
end
class Identity < Grape::Entity
@@ -343,7 +343,7 @@ module API
end
class ProjectGroupLink < Grape::Entity
- expose :id, :project_id, :group_id, :group_access
+ expose :id, :project_id, :group_id, :group_access, :expires_at
end
class Todo < Grape::Entity
@@ -432,8 +432,11 @@ module API
end
end
- class Label < Grape::Entity
+ class LabelBasic < Grape::Entity
expose :name, :color, :description
+ end
+
+ class Label < LabelBasic
expose :open_issues_count, :closed_issues_count, :open_merge_requests_count
expose :subscribed do |label, options|
@@ -441,6 +444,19 @@ module API
end
end
+ class List < Grape::Entity
+ expose :id
+ expose :label, using: Entities::LabelBasic
+ expose :position
+ end
+
+ class Board < Grape::Entity
+ expose :id
+ expose :lists, using: Entities::List do |board|
+ board.lists.destroyable
+ end
+ end
+
class Compare < Grape::Entity
expose :commit, using: Entities::RepoCommit do |compare, options|
Commit.decorate(compare.commits, nil).last
@@ -494,6 +510,8 @@ module API
expose :after_sign_out_path
expose :container_registry_token_expire_delay
expose :repository_storage
+ expose :koding_enabled
+ expose :koding_url
end
class Release < Grape::Entity
@@ -545,6 +563,10 @@ module API
expose :filename, :size
end
+ class PipelineBasic < Grape::Entity
+ expose :id, :sha, :ref, :status
+ end
+
class Build < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
@@ -552,6 +574,7 @@ module API
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
expose :commit, with: RepoCommit
expose :runner, with: Runner
+ expose :pipeline, with: PipelineBasic
end
class Trigger < Grape::Entity
@@ -562,8 +585,8 @@ module API
expose :key, :value
end
- class Pipeline < Grape::Entity
- expose :id, :status, :ref, :sha, :before_sha, :tag, :yaml_errors
+ class Pipeline < PipelineBasic
+ expose :before_sha, :tag, :yaml_errors
expose :user, with: Entities::UserBasic
expose :created_at, :updated_at, :started_at, :finished_at, :committed_at
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 953fa474e88..bfb89475025 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -6,6 +6,8 @@ module API
resource :groups do
# Get a groups list
#
+ # Parameters:
+ # skip_groups (optional) - Array of group ids to exclude from list
# Example Request:
# GET /groups
get do
@@ -16,6 +18,7 @@ module API
end
@groups = @groups.search(params[:search]) if params[:search].present?
+ @groups = @groups.where.not(id: params[:skip_groups]) if params[:skip_groups].present?
@groups = paginate @groups
present @groups, with: Entities::Group
end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 714d4ea3dc6..67473f300c9 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -21,8 +21,11 @@ module API
end
# Check the Rails session for valid authentication details
+ #
+ # Until CSRF protection is added to the API, disallow this method for
+ # state-changing endpoints
def find_user_from_warden
- warden ? warden.authenticate : nil
+ warden.try(:authenticate) if %w[GET HEAD].include?(env['REQUEST_METHOD'])
end
def find_user_by_private_token
@@ -430,7 +433,7 @@ module API
end
def secret_token
- File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+ Gitlab::Shell.secret_token
end
def send_git_blob(repository, blob)
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 090d04544da..9a5d1ece070 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -90,7 +90,7 @@ module API
{
username: token_handler.actor_name,
- lfs_token: token_handler.generate,
+ lfs_token: token_handler.token,
repository_http_path: project.http_url_to_repo
}
end
diff --git a/lib/api/keys.rb b/lib/api/keys.rb
index 2b723b79504..767f27ef334 100644
--- a/lib/api/keys.rb
+++ b/lib/api/keys.rb
@@ -4,10 +4,9 @@ module API
before { authenticate! }
resource :keys do
- # Get single ssh key by id. Only available to admin users.
- #
- # Example Request:
- # GET /keys/:id
+ desc 'Get single ssh key by id. Only available to admin users' do
+ success Entities::SSHKeyWithUser
+ end
get ":id" do
authenticated_as_admin!
diff --git a/lib/api/members.rb b/lib/api/members.rb
index 37f0a6512f4..b80818f0eb6 100644
--- a/lib/api/members.rb
+++ b/lib/api/members.rb
@@ -5,16 +5,16 @@ module API
helpers ::API::Helpers::MembersHelpers
%w[group project].each do |source_type|
+ params do
+ requires :id, type: String, desc: "The #{source_type} ID"
+ end
resource source_type.pluralize do
- # Get a list of group/project members viewable by the authenticated user.
- #
- # Parameters:
- # id (required) - The group/project ID
- # query - Query string
- #
- # Example Request:
- # GET /groups/:id/members
- # GET /projects/:id/members
+ desc 'Gets a list of group or project members viewable by the authenticated user.' do
+ success Entities::Member
+ end
+ params do
+ optional :query, type: String, desc: 'A query string to search for members'
+ end
get ":id/members" do
source = find_source(source_type, params[:id])
@@ -25,15 +25,12 @@ module API
present users, with: Entities::Member, source: source
end
- # Get a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- #
- # Example Request:
- # GET /groups/:id/members/:user_id
- # GET /projects/:id/members/:user_id
+ desc 'Gets a member of a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
get ":id/members/:user_id" do
source = find_source(source_type, params[:id])
@@ -43,48 +40,34 @@ module API
present member.user, with: Entities::Member, member: member
end
- # Add a new group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the new member
- # access_level (required) - A valid access level
- # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
- #
- # Example Request:
- # POST /groups/:id/members
- # POST /projects/:id/members
+ desc 'Adds a member to a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level (defaults: `30`, developer access level)'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
post ":id/members" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
- required_attributes! [:user_id, :access_level]
-
- access_requester = source.requesters.find_by(user_id: params[:user_id])
- if access_requester
- # We pass current_user = access_requester so that the requester doesn't
- # receive a "access denied" email
- ::Members::DestroyService.new(access_requester, access_requester.user).execute
- end
member = source.members.find_by(user_id: params[:user_id])
- # This is to ensure back-compatibility but 409 behavior should be used
- # for both project and group members in 9.0!
+ # We need this explicit check because `source.add_user` doesn't
+ # currently return the member created so it would return 201 even if
+ # the member already existed...
+ # The `source_type == 'group'` check is to ensure back-compatibility
+ # but 409 behavior should be used for both project and group members in 9.0!
conflict!('Member already exists') if source_type == 'group' && member
unless member
- source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
- member = source.members.find_by(user_id: params[:user_id])
+ member = source.add_user(params[:user_id], params[:access_level], current_user: current_user, expires_at: params[:expires_at])
end
- if member
+ if member.persisted? && member.valid?
present member.user, with: Entities::Member, member: member
else
- # Since `source.add_user` doesn't return a member object, we have to
- # build a new one and populate its errors in order to render them.
- member = source.members.build(attributes_for_keys([:user_id, :access_level, :expires_at]))
- member.valid? # populate the errors
-
# This is to ensure back-compatibility but 400 behavior should be used
# for all validation errors in 9.0!
render_api_error!('Access level is not known', 422) if member.errors.key?(:access_level)
@@ -92,21 +75,17 @@ module API
end
end
- # Update a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- # access_level (required) - A valid access level
- # expires_at (optional) - Date string in the format YEAR-MONTH-DAY
- #
- # Example Request:
- # PUT /groups/:id/members/:user_id
- # PUT /projects/:id/members/:user_id
+ desc 'Updates a member of a group or project.' do
+ success Entities::Member
+ end
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the new member'
+ requires :access_level, type: Integer, desc: 'A valid access level'
+ optional :expires_at, type: DateTime, desc: 'Date string in the format YEAR-MONTH-DAY'
+ end
put ":id/members/:user_id" do
source = find_source(source_type, params[:id])
authorize_admin_source!(source_type, source)
- required_attributes! [:user_id, :access_level]
member = source.members.find_by!(user_id: params[:user_id])
attrs = attributes_for_keys [:access_level, :expires_at]
@@ -121,18 +100,12 @@ module API
end
end
- # Remove a group/project member
- #
- # Parameters:
- # id (required) - The group/project ID
- # user_id (required) - The user ID of the member
- #
- # Example Request:
- # DELETE /groups/:id/members/:user_id
- # DELETE /projects/:id/members/:user_id
+ desc 'Removes a user from a group or project.'
+ params do
+ requires :user_id, type: Integer, desc: 'The user ID of the member'
+ end
delete ":id/members/:user_id" do
source = find_source(source_type, params[:id])
- required_attributes! [:user_id]
# This is to ensure back-compatibility but find_by! should be used
# in that casse in 9.0!
@@ -147,7 +120,7 @@ module API
if member.nil?
{ message: "Access revoked", id: params[:user_id].to_i }
else
- ::Members::DestroyService.new(member, current_user).execute
+ ::Members::DestroyService.new(source, current_user, declared(params)).execute
present member.user, with: Entities::Member, member: member
end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
index 7a0cb7c99f3..9b73f6826cf 100644
--- a/lib/api/milestones.rb
+++ b/lib/api/milestones.rb
@@ -108,8 +108,7 @@ module API
finder_params = {
project_id: user_project.id,
- milestone_title: @milestone.title,
- state: 'all'
+ milestone_title: @milestone.title
}
issues = IssuesFinder.new(current_user, finder_params).execute
diff --git a/lib/api/namespaces.rb b/lib/api/namespaces.rb
index 50d3729449e..fe981d7b9fa 100644
--- a/lib/api/namespaces.rb
+++ b/lib/api/namespaces.rb
@@ -4,20 +4,18 @@ module API
before { authenticate! }
resource :namespaces do
- # Get a namespaces list
- #
- # Example Request:
- # GET /namespaces
+ desc 'Get a namespaces list' do
+ success Entities::Namespace
+ end
+ params do
+ optional :search, type: String, desc: "Search query for namespaces"
+ end
get do
- @namespaces = if current_user.admin
- Namespace.all
- else
- current_user.namespaces
- end
- @namespaces = @namespaces.search(params[:search]) if params[:search].present?
- @namespaces = paginate @namespaces
+ namespaces = current_user.admin ? Namespace.all : current_user.namespaces
+
+ namespaces = namespaces.search(params[:search]) if params[:search].present?
- present @namespaces, with: Entities::Namespace
+ present paginate(namespaces), with: Entities::Namespace
end
end
end
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index 6d99617b56f..c24e8e8bd9b 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -22,14 +22,25 @@ module API
# Example Request:
# GET /projects
get do
- @projects = current_user.authorized_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- if params[:simple]
- present @projects, with: Entities::BasicProjectDetails, user: current_user
- else
- present @projects, with: Entities::ProjectWithAccess, user: current_user
- end
+ projects = current_user.authorized_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present projects, with: entity, user: current_user
+ end
+
+ # Get a list of visible projects for authenticated user
+ #
+ # Example Request:
+ # GET /projects/visible
+ get '/visible' do
+ projects = ProjectsFinder.new.execute(current_user)
+ projects = filter_projects(projects)
+ projects = paginate projects
+ entity = params[:simple] ? Entities::BasicProjectDetails : Entities::ProjectWithAccess
+
+ present projects, with: entity, user: current_user
end
# Get an owned projects list for authenticated user
@@ -37,10 +48,10 @@ module API
# Example Request:
# GET /projects/owned
get '/owned' do
- @projects = current_user.owned_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ projects = current_user.owned_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::ProjectWithAccess, user: current_user
end
# Gets starred project for the authenticated user
@@ -48,10 +59,10 @@ module API
# Example Request:
# GET /projects/starred
get '/starred' do
- @projects = current_user.viewable_starred_projects
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::Project, user: current_user
+ projects = current_user.viewable_starred_projects
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::Project, user: current_user
end
# Get all projects for admin user
@@ -60,10 +71,10 @@ module API
# GET /projects/all
get '/all' do
authenticated_as_admin!
- @projects = Project.all
- @projects = filter_projects(@projects)
- @projects = paginate @projects
- present @projects, with: Entities::ProjectWithAccess, user: current_user
+ projects = Project.all
+ projects = filter_projects(projects)
+ projects = paginate projects
+ present projects, with: Entities::ProjectWithAccess, user: current_user
end
# Get a single project
@@ -393,23 +404,24 @@ module API
# Share project with group
#
# Parameters:
- # id (required) - The ID of a project
- # group_id (required) - The ID of a group
+ # id (required) - The ID of a project
+ # group_id (required) - The ID of a group
# group_access (required) - Level of permissions for sharing
+ # expires_at (optional) - Share expiration date
#
# Example Request:
# POST /projects/:id/share
post ":id/share" do
authorize! :admin_project, user_project
required_attributes! [:group_id, :group_access]
+ attrs = attributes_for_keys [:group_id, :group_access, :expires_at]
unless user_project.allowed_to_share_with_group?
return render_api_error!("The project sharing with group is disabled", 400)
end
- link = user_project.project_group_links.new
- link.group_id = params[:group_id]
- link.group_access = params[:group_access]
+ link = user_project.project_group_links.new(attrs)
+
if link.save
present link, with: Entities::ProjectGroupLink
else
diff --git a/lib/api/users.rb b/lib/api/users.rb
index c440305ff0f..18c4cad09ae 100644
--- a/lib/api/users.rb
+++ b/lib/api/users.rb
@@ -60,6 +60,7 @@ module API
# linkedin - Linkedin
# twitter - Twitter account
# website_url - Website url
+ # organization - Organization
# projects_limit - Number of projects user can create
# extern_uid - External authentication provider UID
# provider - External provider
@@ -74,7 +75,7 @@ module API
post do
authenticated_as_admin!
required_attributes! [:email, :password, :name, :username]
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :confirm, :external, :organization]
admin = attrs.delete(:admin)
confirm = !(attrs.delete(:confirm) =~ /(false|f|no|0)$/i)
user = User.build_user(attrs)
@@ -111,6 +112,7 @@ module API
# linkedin - Linkedin
# twitter - Twitter account
# website_url - Website url
+ # organization - Organization
# projects_limit - Limit projects each user can create
# bio - Bio
# location - Location of the user
@@ -122,7 +124,7 @@ module API
put ":id" do
authenticated_as_admin!
- attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external]
+ attrs = attributes_for_keys [:email, :name, :password, :skype, :linkedin, :twitter, :website_url, :projects_limit, :username, :bio, :location, :can_create_group, :admin, :external, :organization]
user = User.find(params[:id])
not_found!('User') unless user
diff --git a/lib/banzai.rb b/lib/banzai.rb
index 9ebe379f454..35ca234c1ba 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -3,6 +3,10 @@ module Banzai
Renderer.render(text, context)
end
+ def self.render_field(object, field)
+ Renderer.render_field(object, field)
+ end
+
def self.cache_collection_render(texts_and_contexts)
Renderer.cache_collection_render(texts_and_contexts)
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 16cd774c81a..affe34394c2 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -64,7 +64,7 @@ module Banzai
end
end
- def project_from_ref_cache(ref)
+ def project_from_ref_cached(ref)
if RequestStore.active?
cache = project_refs_cache
@@ -146,7 +146,7 @@ module Banzai
# have `gfm` and `gfm-OBJECT_NAME` class names attached for styling.
def object_link_filter(text, pattern, link_text: nil)
references_in(text, pattern) do |match, id, project_ref, matches|
- project = project_from_ref_cache(project_ref)
+ project = project_from_ref_cached(project_ref)
if project && object = find_object_cached(project, id)
title = object_link_title(object)
@@ -243,11 +243,27 @@ module Banzai
end
end
- # Returns the projects for the given paths.
- def find_projects_for_paths(paths)
+ def projects_relation_for_paths(paths)
Project.where_paths_in(paths).includes(:namespace)
end
+ # Returns projects for the given paths.
+ def find_projects_for_paths(paths)
+ if RequestStore.active?
+ to_query = paths - project_refs_cache.keys
+
+ unless to_query.empty?
+ projects_relation_for_paths(to_query).each do |project|
+ get_or_set_cache(project_refs_cache, project.path_with_namespace) { project }
+ end
+ end
+
+ project_refs_cache.slice(*paths).values
+ else
+ projects_relation_for_paths(paths)
+ end
+ end
+
def current_project_path
@current_project_path ||= project.path_with_namespace
end
diff --git a/lib/banzai/filter/html_entity_filter.rb b/lib/banzai/filter/html_entity_filter.rb
new file mode 100644
index 00000000000..e008fd428b0
--- /dev/null
+++ b/lib/banzai/filter/html_entity_filter.rb
@@ -0,0 +1,12 @@
+require 'erb'
+
+module Banzai
+ module Filter
+ # Text filter that escapes these HTML entities: & " < >
+ class HtmlEntityFilter < HTML::Pipeline::TextFilter
+ def call
+ ERB::Util.html_escape(text)
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index 4042e9a4c25..54c5f9a71a4 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -66,7 +66,7 @@ module Banzai
end
end
- def find_projects_for_paths(paths)
+ def projects_relation_for_paths(paths)
super(paths).includes(:gitlab_issue_tracker_service)
end
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 2470362e019..af1e575fc89 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -25,7 +25,7 @@ module Banzai
return if customized?(whitelist[:transformers])
# Allow code highlighting
- whitelist[:attributes]['pre'] = %w(class)
+ whitelist[:attributes]['pre'] = %w(class v-pre)
whitelist[:attributes]['span'] = %w(class)
# Allow table alignment
diff --git a/lib/banzai/filter/syntax_highlight_filter.rb b/lib/banzai/filter/syntax_highlight_filter.rb
index fcdb496aed2..026b81ac175 100644
--- a/lib/banzai/filter/syntax_highlight_filter.rb
+++ b/lib/banzai/filter/syntax_highlight_filter.rb
@@ -30,7 +30,7 @@ module Banzai
# users can still access an issue/comment/etc.
end
- highlighted = %(<pre class="#{css_classes}"><code>#{code}</code></pre>)
+ highlighted = %(<pre class="#{css_classes}" v-pre="true"><code>#{code}</code></pre>)
# Extracted to a method to measure it
replace_parent_pre_element(node, highlighted)
diff --git a/lib/banzai/filter/task_list_filter.rb b/lib/banzai/filter/task_list_filter.rb
index 66608c9859c..9fa5f589f3e 100644
--- a/lib/banzai/filter/task_list_filter.rb
+++ b/lib/banzai/filter/task_list_filter.rb
@@ -2,27 +2,7 @@ require 'task_list/filter'
module Banzai
module Filter
- # Work around a bug in the default TaskList::Filter that adds a `task-list`
- # class to every list element, regardless of whether or not it contains a
- # task list.
- #
- # This is a (hopefully) temporary fix, pending a new release of the
- # task_list gem.
- #
- # See https://github.com/github/task_list/pull/60
class TaskListFilter < TaskList::Filter
- def add_css_class_with_fix(node, *new_class_names)
- if new_class_names.include?('task-list')
- # Don't add class to all lists
- return
- elsif new_class_names.include?('task-list-item')
- add_css_class_without_fix(node.parent, 'task-list')
- end
-
- add_css_class_without_fix(node, *new_class_names)
- end
-
- alias_method_chain :add_css_class, :fix
end
end
end
diff --git a/lib/banzai/filter/user_reference_filter.rb b/lib/banzai/filter/user_reference_filter.rb
index e1ca7f4d24b..c6302b586d3 100644
--- a/lib/banzai/filter/user_reference_filter.rb
+++ b/lib/banzai/filter/user_reference_filter.rb
@@ -106,13 +106,17 @@ module Banzai
project = context[:project]
author = context[:author]
- url = urls.namespace_project_url(project.namespace, project,
- only_path: context[:only_path])
+ if author && !project.team.member?(author)
+ link_text
+ else
+ url = urls.namespace_project_url(project.namespace, project,
+ only_path: context[:only_path])
- data = data_attribute(project: project.id, author: author.try(:id))
- text = link_text || User.reference_prefix + 'all'
+ data = data_attribute(project: project.id, author: author.try(:id))
+ text = link_text || User.reference_prefix + 'all'
- link_tag(url, data, text, 'All Project and Group Members')
+ link_tag(url, data, text, 'All Project and Group Members')
+ end
end
def link_to_namespace(namespace, link_text: nil)
diff --git a/lib/banzai/note_renderer.rb b/lib/banzai/note_renderer.rb
index bab6a9934d1..2b7c10f1a0e 100644
--- a/lib/banzai/note_renderer.rb
+++ b/lib/banzai/note_renderer.rb
@@ -3,7 +3,7 @@ module Banzai
# Renders a collection of Note instances.
#
# notes - The notes to render.
- # project - The project to use for rendering/redacting.
+ # project - The project to use for redacting.
# user - The user viewing the notes.
# path - The request path.
# wiki - The project's wiki.
@@ -13,8 +13,7 @@ module Banzai
user,
requested_path: path,
project_wiki: wiki,
- ref: git_ref,
- pipeline: :note)
+ ref: git_ref)
renderer.render(notes, :note)
end
diff --git a/lib/banzai/object_renderer.rb b/lib/banzai/object_renderer.rb
index 9aef807c152..9f8eb0931b8 100644
--- a/lib/banzai/object_renderer.rb
+++ b/lib/banzai/object_renderer.rb
@@ -1,28 +1,32 @@
module Banzai
- # Class for rendering multiple objects (e.g. Note instances) in a single pass.
+ # Class for rendering multiple objects (e.g. Note instances) in a single pass,
+ # using +render_field+ to benefit from caching in the database. Rendering and
+ # redaction are both performed.
#
- # Rendered Markdown is stored in an attribute in every object based on the
- # name of the attribute containing the Markdown. For example, when the
- # attribute `note` is rendered the HTML is stored in `note_html`.
+ # The unredacted HTML is generated according to the usual +render_field+
+ # policy, so specify the pipeline and any other context options on the model.
+ #
+ # The *redacted* (i.e., suitable for use) HTML is placed in an attribute
+ # named "redacted_<foo>", where <foo> is the name of the cache field for the
+ # chosen attribute.
+ #
+ # As an example, rendering the attribute `note` would place the unredacted
+ # HTML into `note_html` and the redacted HTML into `redacted_note_html`.
class ObjectRenderer
attr_reader :project, :user
- # Make sure to set the appropriate pipeline in the `raw_context` attribute
- # (e.g. `:note` for Note instances).
- #
- # project - A Project to use for rendering and redacting Markdown.
+ # project - A Project to use for redacting Markdown.
# user - The user viewing the Markdown/HTML documents, if any.
- # context - A Hash containing extra attributes to use in the rendering
- # pipeline.
- def initialize(project, user = nil, raw_context = {})
+ # context - A Hash containing extra attributes to use during redaction
+ def initialize(project, user = nil, redaction_context = {})
@project = project
@user = user
- @raw_context = raw_context
+ @redaction_context = redaction_context
end
# Renders and redacts an Array of objects.
#
- # objects - The objects to render
+ # objects - The objects to render.
# attribute - The attribute containing the raw Markdown to render.
#
# Returns the same input objects.
@@ -32,7 +36,7 @@ module Banzai
objects.each_with_index do |object, index|
redacted_data = redacted[index]
- object.__send__("#{attribute}_html=", redacted_data[:document].to_html.html_safe)
+ object.__send__("redacted_#{attribute}_html=", redacted_data[:document].to_html.html_safe)
object.user_visible_reference_count = redacted_data[:visible_reference_count]
end
end
@@ -53,12 +57,8 @@ module Banzai
# Returns a Banzai context for the given object and attribute.
def context_for(object, attribute)
- context = base_context.merge(cache_key: [object, attribute])
-
- if object.respond_to?(:author)
- context[:author] = object.author
- end
-
+ context = base_context.dup
+ context = context.merge(object.banzai_render_context(attribute))
context
end
@@ -66,21 +66,16 @@ module Banzai
#
# Returns an Array of `Nokogiri::HTML::Document`.
def render_attributes(objects, attribute)
- strings_and_contexts = objects.map do |object|
+ objects.map do |object|
+ string = Banzai.render_field(object, attribute)
context = context_for(object, attribute)
- string = object.__send__(attribute)
-
- { text: string, context: context }
- end
-
- Banzai.cache_collection_render(strings_and_contexts).each_with_index.map do |html, index|
- Banzai::Pipeline[:relative_link].to_document(html, strings_and_contexts[index][:context])
+ Banzai::Pipeline[:relative_link].to_document(string, context)
end
end
def base_context
- @base_context ||= @raw_context.merge(current_user: user, project: project)
+ @base_context ||= @redaction_context.merge(current_user: user, project: project)
end
end
end
diff --git a/lib/banzai/pipeline/single_line_pipeline.rb b/lib/banzai/pipeline/single_line_pipeline.rb
index ba2555df98d..1929099931b 100644
--- a/lib/banzai/pipeline/single_line_pipeline.rb
+++ b/lib/banzai/pipeline/single_line_pipeline.rb
@@ -3,6 +3,7 @@ module Banzai
class SingleLinePipeline < GfmPipeline
def self.filters
@filters ||= FilterArray[
+ Filter::HtmlEntityFilter,
Filter::SanitizationFilter,
Filter::EmojiFilter,
diff --git a/lib/banzai/reference_parser/base_parser.rb b/lib/banzai/reference_parser/base_parser.rb
index e8e03e4a98f..f5d110e987b 100644
--- a/lib/banzai/reference_parser/base_parser.rb
+++ b/lib/banzai/reference_parser/base_parser.rb
@@ -79,7 +79,11 @@ module Banzai
def referenced_by(nodes)
ids = unique_attribute_values(nodes, self.class.data_attribute)
- references_relation.where(id: ids)
+ if ids.empty?
+ references_relation.none
+ else
+ references_relation.where(id: ids)
+ end
end
# Returns the ActiveRecord::Relation to use for querying references in the
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index a4ae27eefd8..6924a293da8 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -31,6 +31,34 @@ module Banzai
end
end
+ # Convert a Markdown-containing field on an object into an HTML-safe String
+ # of HTML. This method is analogous to calling render(object.field), but it
+ # can cache the rendered HTML in the object, rather than Redis.
+ #
+ # The context to use is learned from the passed-in object by calling
+ # #banzai_render_context(field), and cannot be changed. Use #render, passing
+ # it the field text, if a custom rendering is needed. The generated context
+ # is returned along with the HTML.
+ def render_field(object, field)
+ html_field = object.markdown_cache_field_for(field)
+
+ html = object.__send__(html_field)
+ return html if html.present?
+
+ html = cacheless_render_field(object, field)
+ object.update_column(html_field, html) unless object.new_record? || object.destroyed?
+
+ html
+ end
+
+ # Same as +render_field+, but without consulting or updating the cache field
+ def cacheless_render_field(object, field)
+ text = object.__send__(field)
+ context = object.banzai_render_context(field)
+
+ cacheless_render(text, context)
+ end
+
# Perform multiple render from an Array of Markdown String into an
# Array of HTML-safe String of HTML.
#
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 59f85416ee5..ed87a2603e8 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -12,10 +12,9 @@ module Ci
# POST /builds/register
post "register" do
authenticate_runner!
- update_runner_last_contact(save: false)
- update_runner_info
required_attributes! [:token]
not_found! unless current_runner.active?
+ update_runner_info
build = Ci::RegisterBuildService.new.execute(current_runner)
@@ -41,10 +40,11 @@ module Ci
# PUT /builds/:id
put ":id" do
authenticate_runner!
- update_runner_last_contact
build = Ci::Build.where(runner_id: current_runner.id).running.find(params[:id])
forbidden!('Build has been erased!') if build.erased?
+ update_runner_info
+
build.update_attributes(trace: params[:trace]) if params[:trace]
Gitlab::Metrics.add_event(:update_build,
diff --git a/lib/ci/api/helpers.rb b/lib/ci/api/helpers.rb
index 23353c62885..e608f5f6cad 100644
--- a/lib/ci/api/helpers.rb
+++ b/lib/ci/api/helpers.rb
@@ -3,7 +3,7 @@ module Ci
module Helpers
BUILD_TOKEN_HEADER = "HTTP_BUILD_TOKEN"
BUILD_TOKEN_PARAM = :token
- UPDATE_RUNNER_EVERY = 40 * 60
+ UPDATE_RUNNER_EVERY = 10 * 60
def authenticate_runners!
forbidden! unless runner_registration_token_valid?
@@ -30,14 +30,22 @@ module Ci
token && (build.valid_token?(token) || build.project.valid_runners_token?(token))
end
- def update_runner_last_contact(save: true)
- # Use a random threshold to prevent beating DB updates
- # it generates a distribution between: [40m, 80m]
+ def update_runner_info
+ return unless update_runner?
+
+ current_runner.contacted_at = Time.now
+ current_runner.assign_attributes(get_runner_version_from_params)
+ current_runner.save if current_runner.changed?
+ end
+
+ def update_runner?
+ # Use a random threshold to prevent beating DB updates.
+ # It generates a distribution between [40m, 80m].
+ #
contacted_at_max_age = UPDATE_RUNNER_EVERY + Random.rand(UPDATE_RUNNER_EVERY)
- if current_runner.contacted_at.nil? || Time.now - current_runner.contacted_at >= contacted_at_max_age
- current_runner.contacted_at = Time.now
- current_runner.save if current_runner.changed? && save
- end
+
+ current_runner.contacted_at.nil? ||
+ (Time.now - current_runner.contacted_at) >= contacted_at_max_age
end
def build_not_found!
@@ -57,11 +65,6 @@ module Ci
attributes_for_keys(["name", "version", "revision", "platform", "architecture"], params["info"])
end
- def update_runner_info
- current_runner.assign_attributes(get_runner_version_from_params)
- current_runner.save if current_runner.changed?
- end
-
def max_artifacts_size
current_application_settings.max_artifacts_size.megabytes.to_i
end
diff --git a/lib/ci/gitlab_ci_yaml_processor.rb b/lib/ci/gitlab_ci_yaml_processor.rb
index 0369e80312a..2fd1fced65c 100644
--- a/lib/ci/gitlab_ci_yaml_processor.rb
+++ b/lib/ci/gitlab_ci_yaml_processor.rb
@@ -4,7 +4,7 @@ module Ci
include Gitlab::Ci::Config::Node::LegacyValidationHelpers
- attr_reader :path, :cache, :stages
+ attr_reader :path, :cache, :stages, :jobs
def initialize(config, path = nil)
@ci_config = Gitlab::Ci::Config.new(config)
diff --git a/lib/ci/version_info.rb b/lib/ci/version_info.rb
deleted file mode 100644
index 2a87c91db5e..00000000000
--- a/lib/ci/version_info.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-class VersionInfo
- include Comparable
-
- attr_reader :major, :minor, :patch
-
- def self.parse(str)
- if str && m = str.match(/(\d+)\.(\d+)\.(\d+)/)
- VersionInfo.new(m[1].to_i, m[2].to_i, m[3].to_i)
- else
- VersionInfo.new
- end
- end
-
- def initialize(major = 0, minor = 0, patch = 0)
- @major = major
- @minor = minor
- @patch = patch
- end
-
- def <=>(other)
- return unless other.is_a? VersionInfo
- return unless valid? && other.valid?
-
- if other.major < @major
- 1
- elsif @major < other.major
- -1
- elsif other.minor < @minor
- 1
- elsif @minor < other.minor
- -1
- elsif other.patch < @patch
- 1
- elsif @patch < other.patch
- -1
- else
- 0
- end
- end
-
- def to_s
- if valid?
- "%d.%d.%d" % [@major, @minor, @patch]
- else
- "Unknown"
- end
- end
-
- def valid?
- @major >= 0 && @minor >= 0 && @patch >= 0 && @major + @minor + @patch > 0
- end
-end
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
new file mode 100644
index 00000000000..ca39b1961ae
--- /dev/null
+++ b/lib/constraints/group_url_constrainer.rb
@@ -0,0 +1,7 @@
+require 'constraints/namespace_url_constrainer'
+
+class GroupUrlConstrainer < NamespaceUrlConstrainer
+ def find_resource(id)
+ Group.find_by_path(id)
+ end
+end
diff --git a/lib/constraints/namespace_url_constrainer.rb b/lib/constraints/namespace_url_constrainer.rb
new file mode 100644
index 00000000000..23920193743
--- /dev/null
+++ b/lib/constraints/namespace_url_constrainer.rb
@@ -0,0 +1,13 @@
+class NamespaceUrlConstrainer
+ def matches?(request)
+ id = request.path.sub(/\A\/+/, '').split('/').first.sub(/.atom\z/, '')
+
+ if id =~ Gitlab::Regex.namespace_regex
+ find_resource(id)
+ end
+ end
+
+ def find_resource(id)
+ Namespace.find_by_path(id)
+ end
+end
diff --git a/lib/constraints/user_url_constrainer.rb b/lib/constraints/user_url_constrainer.rb
new file mode 100644
index 00000000000..504a0f5d93e
--- /dev/null
+++ b/lib/constraints/user_url_constrainer.rb
@@ -0,0 +1,7 @@
+require 'constraints/namespace_url_constrainer'
+
+class UserUrlConstrainer < NamespaceUrlConstrainer
+ def find_resource(id)
+ User.find_by('lower(username) = ?', id.downcase)
+ end
+end
diff --git a/lib/event_filter.rb b/lib/event_filter.rb
index 668d2fa41b3..96e70e37e8f 100644
--- a/lib/event_filter.rb
+++ b/lib/event_filter.rb
@@ -2,8 +2,8 @@ class EventFilter
attr_accessor :params
class << self
- def default_filter
- %w{ push issues merge_requests team}
+ def all
+ 'all'
end
def push
@@ -35,18 +35,21 @@ class EventFilter
return events unless params.present?
filter = params.dup
-
actions = []
- actions << Event::PUSHED if filter.include? 'push'
- actions << Event::MERGED if filter.include? 'merged'
- if filter.include? 'team'
- actions << Event::JOINED
- actions << Event::LEFT
+ case filter
+ when EventFilter.push
+ actions = [Event::PUSHED]
+ when EventFilter.merged
+ actions = [Event::MERGED]
+ when EventFilter.comments
+ actions = [Event::COMMENTED]
+ when EventFilter.team
+ actions = [Event::JOINED, Event::LEFT]
+ when EventFilter.all
+ actions = [Event::PUSHED, Event::MERGED, Event::COMMENTED, Event::JOINED, Event::LEFT]
end
- actions << Event::COMMENTED if filter.include? 'comments'
-
events.where(action: actions)
end
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index a533bac2692..9b484a2ecfd 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -53,6 +53,10 @@ module Gitlab
}
end
+ def sym_options_with_owner
+ sym_options.merge(owner: OWNER)
+ end
+
def protection_options
{
"Not protected: Both developers and masters can push new commits, force push, or delete the branch." => PROTECTION_NONE,
diff --git a/lib/gitlab/auth.rb b/lib/gitlab/auth.rb
index 7c0f2115d43..aca5d0020cf 100644
--- a/lib/gitlab/auth.rb
+++ b/lib/gitlab/auth.rb
@@ -124,7 +124,7 @@ module Gitlab
read_authentication_abilities
end
- Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.value, password)
+ Result.new(actor, nil, token_handler.type, authentication_abilities) if Devise.secure_compare(token_handler.token, password)
end
def build_access_token_check(login, password)
diff --git a/lib/gitlab/backend/shell.rb b/lib/gitlab/backend/shell.rb
index 79eac66b364..d0060fbaca1 100644
--- a/lib/gitlab/backend/shell.rb
+++ b/lib/gitlab/backend/shell.rb
@@ -17,6 +17,18 @@ module Gitlab
end
class << self
+ def secret_token
+ @secret_token ||= begin
+ File.read(Gitlab.config.gitlab_shell.secret_file).chomp
+ end
+ end
+
+ def ensure_secret_token!
+ return if File.exist?(File.join(Gitlab.config.gitlab_shell.path, '.gitlab_shell_secret'))
+
+ generate_and_link_secret_token
+ end
+
def version_required
@version_required ||= File.read(Rails.root.
join('GITLAB_SHELL_VERSION')).strip
@@ -25,6 +37,25 @@ module Gitlab
def strip_key(key)
key.split(/ /)[0, 2].join(' ')
end
+
+ private
+
+ # Create (if necessary) and link the secret token file
+ def generate_and_link_secret_token
+ secret_file = Gitlab.config.gitlab_shell.secret_file
+ shell_path = Gitlab.config.gitlab_shell.path
+
+ unless File.size?(secret_file)
+ # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
+ token = SecureRandom.hex(16)
+ File.write(secret_file, token)
+ end
+
+ link_path = File.join(shell_path, '.gitlab_shell_secret')
+ if File.exist?(shell_path) && !File.exist?(link_path)
+ FileUtils.symlink(secret_file, link_path)
+ end
+ end
end
# Init new repository
@@ -201,21 +232,6 @@ module Gitlab
File.exist?(full_path(storage, dir_name))
end
- # Create (if necessary) and link the secret token file
- def generate_and_link_secret_token
- secret_file = Gitlab.config.gitlab_shell.secret_file
- unless File.size?(secret_file)
- # Generate a new token of 16 random hexadecimal characters and store it in secret_file.
- token = SecureRandom.hex(16)
- File.write(secret_file, token)
- end
-
- link_path = File.join(gitlab_shell_path, '.gitlab_shell_secret')
- if File.exist?(gitlab_shell_path) && !File.exist?(link_path)
- FileUtils.symlink(secret_file, link_path)
- end
- end
-
protected
def gitlab_shell_path
diff --git a/lib/gitlab/github_import/client.rb b/lib/gitlab/github_import/client.rb
index 084e514492c..7f424b74efb 100644
--- a/lib/gitlab/github_import/client.rb
+++ b/lib/gitlab/github_import/client.rb
@@ -52,7 +52,7 @@ module Gitlab
def method_missing(method, *args, &block)
if api.respond_to?(method)
- request { api.send(method, *args, &block) }
+ request(method, *args, &block)
else
super(method, *args, &block)
end
@@ -99,20 +99,29 @@ module Gitlab
rate_limit.resets_in + GITHUB_SAFE_SLEEP_TIME
end
- def request
+ def request(method, *args, &block)
sleep rate_limit_sleep_time if rate_limit_exceed?
- data = yield
+ data = api.send(method, *args)
+ return data unless data.is_a?(Array)
+ if block_given?
+ yield data
+ each_response_page(&block)
+ else
+ each_response_page { |page| data.concat(page) }
+ data
+ end
+ end
+
+ def each_response_page
last_response = api.last_response
while last_response.rels[:next]
sleep rate_limit_sleep_time if rate_limit_exceed?
last_response = last_response.rels[:next].get
- data.concat(last_response.data) if last_response.data.is_a?(Array)
+ yield last_response.data if last_response.data.is_a?(Array)
end
-
- data
end
end
end
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index d35ee2a1c65..4b70f33a851 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -10,6 +10,7 @@ module Gitlab
@repo = project.import_source
@repo_url = project.import_url
@errors = []
+ @labels = {}
if credentials
@client = Client.new(credentials[:user])
@@ -23,6 +24,7 @@ module Gitlab
import_milestones
import_issues
import_pull_requests
+ import_comments
import_wiki
import_releases
handle_errors
@@ -46,66 +48,68 @@ module Gitlab
end
def import_labels
- labels = client.labels(repo, per_page: 100)
-
- labels.each do |raw|
- begin
- LabelFormatter.new(project, raw).create!
- rescue => e
- errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.labels(repo, per_page: 100) do |labels|
+ labels.each do |raw|
+ begin
+ label = LabelFormatter.new(project, raw).create!
+ @labels[label.title] = label.id
+ rescue => e
+ errors << { type: :label, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
def import_milestones
- milestones = client.milestones(repo, state: :all, per_page: 100)
-
- milestones.each do |raw|
- begin
- MilestoneFormatter.new(project, raw).create!
- rescue => e
- errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.milestones(repo, state: :all, per_page: 100) do |milestones|
+ milestones.each do |raw|
+ begin
+ MilestoneFormatter.new(project, raw).create!
+ rescue => e
+ errors << { type: :milestone, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
def import_issues
- issues = client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
-
- issues.each do |raw|
- gh_issue = IssueFormatter.new(project, raw)
-
- if gh_issue.valid?
- begin
- issue = gh_issue.create!
- apply_labels(issue)
- import_comments(issue) if gh_issue.has_comments?
- rescue => e
- errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.issues(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |issues|
+ issues.each do |raw|
+ gh_issue = IssueFormatter.new(project, raw)
+
+ if gh_issue.valid?
+ begin
+ issue = gh_issue.create!
+ apply_labels(issue, raw)
+ rescue => e
+ errors << { type: :issue, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
end
def import_pull_requests
- pull_requests = client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100)
- pull_requests = pull_requests.map { |raw| PullRequestFormatter.new(project, raw) }.select(&:valid?)
-
- pull_requests.each do |pull_request|
- begin
- restore_source_branch(pull_request) unless pull_request.source_branch_exists?
- restore_target_branch(pull_request) unless pull_request.target_branch_exists?
-
- merge_request = pull_request.create!
- apply_labels(merge_request)
- import_comments(merge_request)
- import_comments_on_diff(merge_request)
- rescue => e
- errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
- ensure
- clean_up_restored_branches(pull_request)
+ client.pull_requests(repo, state: :all, sort: :created, direction: :asc, per_page: 100) do |pull_requests|
+ pull_requests.each do |raw|
+ pull_request = PullRequestFormatter.new(project, raw)
+ next unless pull_request.valid?
+
+ begin
+ restore_source_branch(pull_request) unless pull_request.source_branch_exists?
+ restore_target_branch(pull_request) unless pull_request.target_branch_exists?
+
+ merge_request = pull_request.create!
+ apply_labels(merge_request, raw)
+ rescue => e
+ errors << { type: :pull_request, url: Gitlab::UrlSanitizer.sanitize(pull_request.url), errors: e.message }
+ ensure
+ clean_up_restored_branches(pull_request)
+ end
end
end
+
+ project.repository.after_remove_branch
end
def restore_source_branch(pull_request)
@@ -125,37 +129,38 @@ module Gitlab
def clean_up_restored_branches(pull_request)
remove_branch(pull_request.source_branch_name) unless pull_request.source_branch_exists?
remove_branch(pull_request.target_branch_name) unless pull_request.target_branch_exists?
-
- project.repository.after_remove_branch
end
- def apply_labels(issuable)
- issue = client.issue(repo, issuable.iid)
-
- if issue.labels.count > 0
- label_ids = issue.labels
- .map { |attrs| project.labels.find_by(title: attrs.name).try(:id) }
+ def apply_labels(issuable, raw_issuable)
+ if raw_issuable.labels.count > 0
+ label_ids = raw_issuable.labels
+ .map { |attrs| @labels[attrs.name] }
.compact
issuable.update_attribute(:label_ids, label_ids)
end
end
- def import_comments(issuable)
- comments = client.issue_comments(repo, issuable.iid, per_page: 100)
- create_comments(issuable, comments)
- end
+ def import_comments
+ client.issues_comments(repo, per_page: 100) do |comments|
+ create_comments(comments, :issue)
+ end
- def import_comments_on_diff(merge_request)
- comments = client.pull_request_comments(repo, merge_request.iid, per_page: 100)
- create_comments(merge_request, comments)
+ client.pull_requests_comments(repo, per_page: 100) do |comments|
+ create_comments(comments, :pull_request)
+ end
end
- def create_comments(issuable, comments)
+ def create_comments(comments, issuable_type)
ActiveRecord::Base.no_touching do
comments.each do |raw|
begin
- comment = CommentFormatter.new(project, raw)
+ comment = CommentFormatter.new(project, raw)
+ issuable_class = issuable_type == :issue ? Issue : MergeRequest
+ iid = raw.send("#{issuable_type}_url").split('/').last # GH doesn't return parent ID directly
+ issuable = issuable_class.find_by_iid(iid)
+ next unless issuable
+
issuable.notes.create!(comment.attributes)
rescue => e
errors << { type: :comment, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
@@ -165,10 +170,9 @@ module Gitlab
end
def import_wiki
- unless project.wiki_enabled?
+ unless project.wiki.repository_exists?
wiki = WikiFormatter.new(project)
gitlab_shell.import_repository(project.repository_storage_path, wiki.path_with_namespace, wiki.import_url)
- project.project.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
end
rescue Gitlab::Shell::Error => e
# GitHub error message when the wiki repo has not been created,
@@ -180,13 +184,14 @@ module Gitlab
end
def import_releases
- releases = client.releases(repo, per_page: 100)
- releases.each do |raw|
- begin
- gh_release = ReleaseFormatter.new(project, raw)
- gh_release.create! if gh_release.valid?
- rescue => e
- errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ client.releases(repo, per_page: 100) do |releases|
+ releases.each do |raw|
+ begin
+ gh_release = ReleaseFormatter.new(project, raw)
+ gh_release.create! if gh_release.valid?
+ rescue => e
+ errors << { type: :release, url: Gitlab::UrlSanitizer.sanitize(raw.url), errors: e.message }
+ end
end
end
end
diff --git a/lib/gitlab/github_import/project_creator.rb b/lib/gitlab/github_import/project_creator.rb
index 605abfabdab..a2410068845 100644
--- a/lib/gitlab/github_import/project_creator.rb
+++ b/lib/gitlab/github_import/project_creator.rb
@@ -1,7 +1,7 @@
module Gitlab
module GithubImport
class ProjectCreator
- attr_reader :repo, :namespace, :current_user, :session_data
+ attr_reader :repo, :name, :namespace, :current_user, :session_data
def initialize(repo, name, namespace, current_user, session_data)
@repo = repo
@@ -12,24 +12,37 @@ module Gitlab
end
def execute
- project = ::Projects::CreateService.new(
+ ::Projects::CreateService.new(
current_user,
- name: @name,
- path: @name,
+ name: name,
+ path: name,
description: repo.description,
namespace_id: namespace.id,
- visibility_level: repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility,
+ visibility_level: visibility_level,
import_type: "github",
import_source: repo.full_name,
- import_url: repo.clone_url.sub("https://", "https://#{@session_data[:github_access_token]}@")
+ import_url: import_url,
+ skip_wiki: skip_wiki
).execute
+ end
+
+ private
- # If repo has wiki we'll import it later
- if repo.has_wiki? && project
- project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::DISABLED)
- end
+ def import_url
+ repo.clone_url.sub('https://', "https://#{session_data[:github_access_token]}@")
+ end
+
+ def visibility_level
+ repo.private ? Gitlab::VisibilityLevel::PRIVATE : ApplicationSetting.current.default_project_visibility
+ end
- project
+ #
+ # If the GitHub project repository has wiki, we should not create the
+ # default wiki. Otherwise the GitHub importer will fail because the wiki
+ # repository already exist.
+ #
+ def skip_wiki
+ repo.has_wiki?
end
end
end
diff --git a/lib/gitlab/identifier.rb b/lib/gitlab/identifier.rb
index 3e5d728f3bc..f8809db21aa 100644
--- a/lib/gitlab/identifier.rb
+++ b/lib/gitlab/identifier.rb
@@ -5,19 +5,61 @@ module Gitlab
def identify(identifier, project, newrev)
if identifier.blank?
# Local push from gitlab
- email = project.commit(newrev).author_email rescue nil
- User.find_by(email: email) if email
-
+ identify_using_commit(project, newrev)
elsif identifier =~ /\Auser-\d+\Z/
# git push over http
- user_id = identifier.gsub("user-", "")
- User.find_by(id: user_id)
-
+ identify_using_user(identifier)
elsif identifier =~ /\Akey-\d+\Z/
# git push over ssh
- key_id = identifier.gsub("key-", "")
- Key.find_by(id: key_id).try(:user)
+ identify_using_ssh_key(identifier)
+ end
+ end
+
+ # Tries to identify a user based on a commit SHA.
+ def identify_using_commit(project, ref)
+ commit = project.commit(ref)
+
+ return if !commit || !commit.author_email
+
+ email = commit.author_email
+
+ identify_with_cache(:email, email) do
+ User.find_by(email: email)
end
end
+
+ # Tries to identify a user based on a user identifier (e.g. "user-123").
+ def identify_using_user(identifier)
+ user_id = identifier.gsub("user-", "")
+
+ identify_with_cache(:user, user_id) do
+ User.find_by(id: user_id)
+ end
+ end
+
+ # Tries to identify a user based on an SSH key identifier (e.g. "key-123").
+ def identify_using_ssh_key(identifier)
+ key_id = identifier.gsub("key-", "")
+
+ identify_with_cache(:ssh_key, key_id) do
+ User.find_by_ssh_key_id(key_id)
+ end
+ end
+
+ def identify_with_cache(category, key)
+ if identification_cache[category].key?(key)
+ identification_cache[category][key]
+ else
+ identification_cache[category][key] = yield
+ end
+ end
+
+ def identification_cache
+ @identification_cache ||= {
+ email: {},
+ user: {},
+ ssh_key: {}
+ }
+ end
end
end
diff --git a/lib/gitlab/import_export/attribute_cleaner.rb b/lib/gitlab/import_export/attribute_cleaner.rb
new file mode 100644
index 00000000000..b9e4042220a
--- /dev/null
+++ b/lib/gitlab/import_export/attribute_cleaner.rb
@@ -0,0 +1,13 @@
+module Gitlab
+ module ImportExport
+ class AttributeCleaner
+ ALLOWED_REFERENCES = RelationFactory::PROJECT_REFERENCES + RelationFactory::USER_REFERENCES
+
+ def self.clean!(relation_hash:)
+ relation_hash.reject! do |key, _value|
+ key.end_with?('_id') && !ALLOWED_REFERENCES.include?(key)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/command_line_util.rb b/lib/gitlab/import_export/command_line_util.rb
index e522a0fc8f6..f00c7460e82 100644
--- a/lib/gitlab/import_export/command_line_util.rb
+++ b/lib/gitlab/import_export/command_line_util.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
module CommandLineUtil
+ DEFAULT_MODE = 0700
+
def tar_czf(archive:, dir:)
tar_with_options(archive: archive, dir: dir, options: 'czf')
end
@@ -21,6 +23,11 @@ module Gitlab
execute(%W(#{Gitlab.config.gitlab_shell.path}/bin/create-hooks) + repository_storage_paths_args)
end
+ def mkdir_p(path)
+ FileUtils.mkdir_p(path, mode: DEFAULT_MODE)
+ FileUtils.chmod(DEFAULT_MODE, path)
+ end
+
private
def tar_with_options(archive:, dir:, options:)
@@ -45,7 +52,7 @@ module Gitlab
# if we are copying files, create the destination folder
destination_folder = File.file?(source) ? File.dirname(destination) : destination
- FileUtils.mkdir_p(destination_folder)
+ mkdir_p(destination_folder)
FileUtils.copy_entry(source, destination)
true
end
diff --git a/lib/gitlab/import_export/file_importer.rb b/lib/gitlab/import_export/file_importer.rb
index eca6e5b6d51..113895ba22c 100644
--- a/lib/gitlab/import_export/file_importer.rb
+++ b/lib/gitlab/import_export/file_importer.rb
@@ -15,7 +15,7 @@ module Gitlab
end
def import
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
wait_for_archived_file do
decompress_archive
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index 88803d76623..bb9d1080330 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -1,5 +1,8 @@
# Model relationships to be included in the project import/export
project_tree:
+ - :labels
+ - milestones:
+ - :events
- issues:
- :events
- notes:
@@ -39,9 +42,6 @@ project_tree:
- protected_branches:
- :merge_access_levels
- :push_access_levels
- - :labels
- - milestones:
- - :events
- :project_feature
# Only include the following attributes for the models specified.
@@ -73,5 +73,7 @@ excluded_attributes:
methods:
statuses:
- :type
+ services:
+ - :type
merge_request_diff:
- :utf8_st_diffs
diff --git a/lib/gitlab/import_export/project_tree_restorer.rb b/lib/gitlab/import_export/project_tree_restorer.rb
index c7b3551b84c..5a109f24f9f 100644
--- a/lib/gitlab/import_export/project_tree_restorer.rb
+++ b/lib/gitlab/import_export/project_tree_restorer.rb
@@ -61,11 +61,17 @@ module Gitlab
def restore_project
return @project unless @tree_hash
- project_params = @tree_hash.reject { |_key, value| value.is_a?(Array) }
@project.update(project_params)
@project
end
+ def project_params
+ @tree_hash.reject do |key, value|
+ # return params that are not 1 to many or 1 to 1 relations
+ value.is_a?(Array) || key == key.singularize
+ end
+ end
+
# Given a relation hash containing one or more models and its relationships,
# loops through each model and each object from a model type and
# and assigns its correspondent attributes hash from +tree_hash+
@@ -104,9 +110,10 @@ module Gitlab
def create_relation(relation, relation_hash_list)
relation_array = [relation_hash_list].flatten.map do |relation_hash|
Gitlab::ImportExport::RelationFactory.create(relation_sym: relation.to_sym,
- relation_hash: relation_hash.merge('project_id' => restored_project.id),
+ relation_hash: relation_hash,
members_mapper: members_mapper,
- user: @user)
+ user: @user,
+ project_id: restored_project.id)
end
relation_hash_list.is_a?(Array) ? relation_array : relation_array.first
diff --git a/lib/gitlab/import_export/project_tree_saver.rb b/lib/gitlab/import_export/project_tree_saver.rb
index 9153088e966..2fbf437ec26 100644
--- a/lib/gitlab/import_export/project_tree_saver.rb
+++ b/lib/gitlab/import_export/project_tree_saver.rb
@@ -1,6 +1,8 @@
module Gitlab
module ImportExport
class ProjectTreeSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
attr_reader :full_path
def initialize(project:, shared:)
@@ -10,7 +12,7 @@ module Gitlab
end
def save
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
File.write(full_path, project_json_tree)
true
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index 354ccd64696..9300f789e1b 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -13,6 +13,8 @@ module Gitlab
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id].freeze
+ PROJECT_REFERENCES = %w[project_id source_project_id gl_project_id target_project_id].freeze
+
BUILD_MODELS = %w[Ci::Build commit_status].freeze
IMPORTED_OBJECT_MAX_RETRIES = 5.freeze
@@ -25,9 +27,9 @@ module Gitlab
new(*args).create
end
- def initialize(relation_sym:, relation_hash:, members_mapper:, user:)
+ def initialize(relation_sym:, relation_hash:, members_mapper:, user:, project_id:)
@relation_name = OVERRIDES[relation_sym] || relation_sym
- @relation_hash = relation_hash.except('id', 'noteable_id')
+ @relation_hash = relation_hash.except('id', 'noteable_id').merge('project_id' => project_id)
@members_mapper = members_mapper
@user = user
@imported_object_retries = 0
@@ -153,7 +155,11 @@ module Gitlab
end
def parsed_relation_hash
- @parsed_relation_hash ||= @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ @parsed_relation_hash ||= begin
+ Gitlab::ImportExport::AttributeCleaner.clean!(relation_hash: @relation_hash)
+
+ @relation_hash.reject { |k, _v| !relation_class.attribute_method?(k) }
+ end
end
def set_st_diffs
diff --git a/lib/gitlab/import_export/repo_restorer.rb b/lib/gitlab/import_export/repo_restorer.rb
index d1e33ea8678..48a9a6fa5e2 100644
--- a/lib/gitlab/import_export/repo_restorer.rb
+++ b/lib/gitlab/import_export/repo_restorer.rb
@@ -12,7 +12,7 @@ module Gitlab
def restore
return true unless File.exist?(@path_to_bundle)
- FileUtils.mkdir_p(path_to_repo)
+ mkdir_p(path_to_repo)
git_unbundle(repo_path: path_to_repo, bundle_path: @path_to_bundle) && repo_restore_hooks
rescue => e
diff --git a/lib/gitlab/import_export/repo_saver.rb b/lib/gitlab/import_export/repo_saver.rb
index 331e14021e6..a7028a32570 100644
--- a/lib/gitlab/import_export/repo_saver.rb
+++ b/lib/gitlab/import_export/repo_saver.rb
@@ -20,7 +20,7 @@ module Gitlab
private
def bundle_to_disk
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: @full_path)
rescue => e
@shared.error(e)
diff --git a/lib/gitlab/import_export/version_saver.rb b/lib/gitlab/import_export/version_saver.rb
index 9b642d740b7..7cf88298642 100644
--- a/lib/gitlab/import_export/version_saver.rb
+++ b/lib/gitlab/import_export/version_saver.rb
@@ -1,12 +1,14 @@
module Gitlab
module ImportExport
class VersionSaver
+ include Gitlab::ImportExport::CommandLineUtil
+
def initialize(shared:)
@shared = shared
end
def save
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
File.write(version_file, Gitlab::ImportExport.version, mode: 'w')
rescue => e
diff --git a/lib/gitlab/import_export/wiki_repo_saver.rb b/lib/gitlab/import_export/wiki_repo_saver.rb
index 6107420e4dd..1e6722a7bba 100644
--- a/lib/gitlab/import_export/wiki_repo_saver.rb
+++ b/lib/gitlab/import_export/wiki_repo_saver.rb
@@ -9,7 +9,7 @@ module Gitlab
end
def bundle_to_disk(full_path)
- FileUtils.mkdir_p(@shared.export_path)
+ mkdir_p(@shared.export_path)
git_bundle(repo_path: path_to_repo, bundle_path: full_path)
rescue => e
@shared.error(e)
diff --git a/lib/gitlab/ldap/access.rb b/lib/gitlab/ldap/access.rb
index 2f326d00a2f..7e06bd2b0fb 100644
--- a/lib/gitlab/ldap/access.rb
+++ b/lib/gitlab/ldap/access.rb
@@ -51,8 +51,6 @@ module Gitlab
user.ldap_block
false
end
- rescue
- false
end
def adapter
diff --git a/lib/gitlab/ldap/adapter.rb b/lib/gitlab/ldap/adapter.rb
index 82cb8cef754..8b38cfaefb6 100644
--- a/lib/gitlab/ldap/adapter.rb
+++ b/lib/gitlab/ldap/adapter.rb
@@ -62,6 +62,9 @@ module Gitlab
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")
[]
diff --git a/lib/gitlab/lfs_token.rb b/lib/gitlab/lfs_token.rb
index d089a2f9b0b..5f67e97fa2a 100644
--- a/lib/gitlab/lfs_token.rb
+++ b/lib/gitlab/lfs_token.rb
@@ -17,19 +17,13 @@ module Gitlab
end
end
- def generate
- token = Devise.friendly_token(TOKEN_LENGTH)
-
+ def token
Gitlab::Redis.with do |redis|
+ token = redis.get(redis_key)
+ token ||= Devise.friendly_token(TOKEN_LENGTH)
redis.set(redis_key, token, ex: EXPIRY_TIME)
- end
- token
- end
-
- def value
- Gitlab::Redis.with do |redis|
- redis.get(redis_key)
+ token
end
end
diff --git a/lib/gitlab/redis.rb b/lib/gitlab/redis.rb
index 69c4ef721d5..c649da8c426 100644
--- a/lib/gitlab/redis.rb
+++ b/lib/gitlab/redis.rb
@@ -11,13 +11,6 @@ module Gitlab
DEFAULT_REDIS_URL = 'redis://localhost:6379'
CONFIG_FILE = File.expand_path('../../config/resque.yml', __dir__)
- # To be thread-safe we must be careful when writing the class instance
- # variables @_raw_config and @pool. Because @pool depends on @_raw_config we need two
- # mutexes to prevent deadlock.
- RAW_CONFIG_MUTEX = Mutex.new
- POOL_MUTEX = Mutex.new
- private_constant :RAW_CONFIG_MUTEX, :POOL_MUTEX
-
class << self
# Do NOT cache in an instance variable. Result may be mutated by caller.
def params
@@ -31,24 +24,29 @@ module Gitlab
end
def with
- if @pool.nil?
- POOL_MUTEX.synchronize do
- @pool = ConnectionPool.new { ::Redis.new(params) }
- end
- end
+ @pool ||= ConnectionPool.new(size: pool_size) { ::Redis.new(params) }
@pool.with { |redis| yield redis }
end
+ def pool_size
+ if Sidekiq.server?
+ # the pool will be used in a multi-threaded context
+ Sidekiq.options[:concurrency] + 5
+ else
+ # probably this is a Unicorn process, so single threaded
+ 5
+ end
+ end
+
def _raw_config
return @_raw_config if defined?(@_raw_config)
- RAW_CONFIG_MUTEX.synchronize do
- begin
- @_raw_config = File.read(CONFIG_FILE).freeze
- rescue Errno::ENOENT
- @_raw_config = false
- end
+ begin
+ @_raw_config = File.read(CONFIG_FILE).freeze
+ rescue Errno::ENOENT
+ @_raw_config = false
end
+
@_raw_config
end
end
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 776bbcbb5d0..0d30e1bb92e 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -2,7 +2,7 @@ module Gitlab
module Regex
extend self
- NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])'.freeze
+ NAMESPACE_REGEX_STR = '(?:[a-zA-Z0-9_\.][a-zA-Z0-9_\-\.]*[a-zA-Z0-9_\-]|[a-zA-Z0-9_])(?<!\.git|\.atom)'.freeze
def namespace_regex
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
@@ -10,7 +10,7 @@ module Gitlab
def namespace_regex_message
"can contain only letters, digits, '_', '-' and '.'. " \
- "Cannot start with '-' or end in '.'." \
+ "Cannot start with '-' or end in '.', '.git' or '.atom'." \
end
def namespace_name_regex
diff --git a/lib/gitlab/sidekiq_middleware/arguments_logger.rb b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
index 7813091ec7b..82a59a7a87e 100644
--- a/lib/gitlab/sidekiq_middleware/arguments_logger.rb
+++ b/lib/gitlab/sidekiq_middleware/arguments_logger.rb
@@ -2,7 +2,7 @@ module Gitlab
module SidekiqMiddleware
class ArgumentsLogger
def call(worker, job, queue)
- Sidekiq.logger.info "arguments: #{job['args']}"
+ Sidekiq.logger.info "arguments: #{JSON.dump(job['args'])}"
yield
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 60aae541d46..594439a5d4b 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -60,7 +60,7 @@ module Gitlab
def send_git_diff(repository, diff_refs)
params = {
'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.start_sha,
+ 'ShaFrom' => diff_refs.base_sha,
'ShaTo' => diff_refs.head_sha
}
@@ -73,7 +73,7 @@ module Gitlab
def send_git_patch(repository, diff_refs)
params = {
'RepoPath' => repository.path_to_repo,
- 'ShaFrom' => diff_refs.start_sha,
+ 'ShaFrom' => diff_refs.base_sha,
'ShaTo' => diff_refs.head_sha
}
@@ -107,15 +107,15 @@ module Gitlab
bytes
end
end
-
+
def write_secret
bytes = SecureRandom.random_bytes(SECRET_LENGTH)
- File.open(secret_path, 'w:BINARY', 0600) do |f|
- f.chmod(0600)
+ File.open(secret_path, 'w:BINARY', 0600) do |f|
+ f.chmod(0600) # If the file already existed, the '0600' passed to 'open' above was a no-op.
f.write(Base64.strict_encode64(bytes))
end
end
-
+
def verify_api_request!(request_headers)
JWT.decode(
request_headers[INTERNAL_API_REQUEST_HEADER],
@@ -128,7 +128,7 @@ module Gitlab
def secret_path
Rails.root.join('.gitlab_workhorse_secret')
end
-
+
protected
def encode(hash)
diff --git a/lib/tasks/cache.rake b/lib/tasks/cache.rake
index 2214f855200..a95a3455a4a 100644
--- a/lib/tasks/cache.rake
+++ b/lib/tasks/cache.rake
@@ -1,22 +1,33 @@
namespace :cache do
- CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
- REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
+ namespace :clear do
+ REDIS_CLEAR_BATCH_SIZE = 1000 # There seems to be no speedup when pushing beyond 1,000
+ REDIS_SCAN_START_STOP = '0' # Magic value, see http://redis.io/commands/scan
- desc "GitLab | Clear redis cache"
- task :clear => :environment do
- Gitlab::Redis.with do |redis|
- cursor = REDIS_SCAN_START_STOP
- loop do
- cursor, keys = redis.scan(
- cursor,
- match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
- count: CLEAR_BATCH_SIZE
- )
-
- redis.del(*keys) if keys.any?
-
- break if cursor == REDIS_SCAN_START_STOP
+ desc "GitLab | Clear redis cache"
+ task redis: :environment do
+ Gitlab::Redis.with do |redis|
+ cursor = REDIS_SCAN_START_STOP
+ loop do
+ cursor, keys = redis.scan(
+ cursor,
+ match: "#{Gitlab::Redis::CACHE_NAMESPACE}*",
+ count: REDIS_CLEAR_BATCH_SIZE
+ )
+
+ redis.del(*keys) if keys.any?
+
+ break if cursor == REDIS_SCAN_START_STOP
+ end
end
end
+
+ desc "GitLab | Clear database cache (in the background)"
+ task db: :environment do
+ ClearDatabaseCacheWorker.perform_async
+ end
+
+ task all: [:db, :redis]
end
+
+ task clear: 'cache:clear:all'
end
diff --git a/lib/tasks/flog.rake b/lib/tasks/flog.rake
deleted file mode 100644
index 3bfe999ae74..00000000000
--- a/lib/tasks/flog.rake
+++ /dev/null
@@ -1,25 +0,0 @@
-desc 'Code complexity analyze via flog'
-task :flog do
- output = %x(bundle exec flog -m app/ lib/gitlab)
- exit_code = 0
- minimum_score = 70
- output = output.lines
-
- # Skip total complexity score
- output.shift
-
- # Skip some trash info
- output.shift
-
- output.each do |line|
- score, method = line.split(" ")
- score = score.to_i
-
- if score > minimum_score
- exit_code = 1
- puts "High complexity in #{method}. Score: #{score}"
- end
- end
-
- exit exit_code
-end
diff --git a/lib/tasks/gitlab/shell.rake b/lib/tasks/gitlab/shell.rake
index bb7eb852f1b..210899882b4 100644
--- a/lib/tasks/gitlab/shell.rake
+++ b/lib/tasks/gitlab/shell.rake
@@ -78,7 +78,7 @@ namespace :gitlab do
f.puts "PATH=#{ENV['PATH']}"
end
- Gitlab::Shell.new.generate_and_link_secret_token
+ Gitlab::Shell.ensure_secret_token!
end
desc "GitLab | Setup gitlab-shell"
diff --git a/lib/tasks/gitlab/users.rake b/lib/tasks/gitlab/users.rake
new file mode 100644
index 00000000000..3a16ace60bd
--- /dev/null
+++ b/lib/tasks/gitlab/users.rake
@@ -0,0 +1,11 @@
+namespace :gitlab do
+ namespace :users do
+ desc "GitLab | Clear the authentication token for all users"
+ task clear_all_authentication_tokens: :environment do |t, args|
+ # Do small batched updates because these updates will be slow and locking
+ User.select(:id).find_in_batches(batch_size: 100) do |batch|
+ User.where(id: batch.map(&:id)).update_all(authentication_token: nil)
+ end
+ end
+ end
+end
diff --git a/public/deploy.html b/public/deploy.html
index 142472b6c35..49ec4ac5ce1 100644
--- a/public/deploy.html
+++ b/public/deploy.html
@@ -2,6 +2,11 @@
<html>
<head>
<meta content="width=device-width, initial-scale=1, maximum-scale=1" name="viewport">
+ <meta name="refresh" content="60">
+ <meta name="retry-after" content="100">
+ <meta name="robots" content="noindex, nofollow, noarchive, nostore">
+ <meta name="cache-control" content="no-cache, no-store">
+ <meta name="pragma" content="no-cache">
<title>Deploy in progress</title>
<style>
body {
@@ -61,4 +66,4 @@
<p>Please contact your GitLab administrator if this problem persists.</p>
</div>
</body>
-</html>
+</html> \ No newline at end of file
diff --git a/public/robots.txt b/public/robots.txt
index 334f4c03533..7d69fad59d1 100644
--- a/public/robots.txt
+++ b/public/robots.txt
@@ -23,7 +23,7 @@ Disallow: /groups/*/edit
Disallow: /users
# Global snippets
-Disallow: /s
+Disallow: /s/
Disallow: /snippets/new
Disallow: /snippets/*/edit
Disallow: /snippets/*/raw
diff --git a/scripts/prepare_build.sh b/scripts/prepare_build.sh
index 76b2178c79c..1eaafdce389 100755
--- a/scripts/prepare_build.sh
+++ b/scripts/prepare_build.sh
@@ -16,21 +16,6 @@ retry() {
}
if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
- mkdir -p vendor/apt
-
- # Install phantomjs package
- pushd vendor/apt
- PHANTOMJS_FILE="phantomjs-$PHANTOMJS_VERSION-linux-x86_64"
- if [ ! -d "$PHANTOMJS_FILE" ]; then
- curl -q -L "https://s3.amazonaws.com/gitlab-build-helpers/$PHANTOMJS_FILE.tar.bz2" | tar jx
- fi
- cp "$PHANTOMJS_FILE/bin/phantomjs" "/usr/bin/"
- popd
-
- # Try to install packages
- retry 'apt-get update -yqqq; apt-get -o dir::cache::archives="vendor/apt" install -y -qq --force-yes \
- libicu-dev libkrb5-dev cmake nodejs postgresql-client mysql-client unzip'
-
cp config/database.yml.mysql config/database.yml
sed -i 's/username:.*/username: root/g' config/database.yml
sed -i 's/password:.*/password:/g' config/database.yml
@@ -39,7 +24,7 @@ if [ -f /.dockerenv ] || [ -f ./dockerinit ]; then
cp config/resque.yml.example config/resque.yml
sed -i 's/localhost/redis/g' config/resque.yml
- export FLAGS=(--path vendor --retry 3)
+ export FLAGS=(--path vendor --retry 3 --quiet)
else
export PATH=$HOME/bin:/usr/local/bin:/usr/bin:/bin
cp config/database.yml.mysql config/database.yml
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index c34475976c6..a0870891cf4 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -2,9 +2,10 @@ require 'spec_helper'
describe Groups::GroupMembersController do
let(:user) { create(:user) }
- let(:group) { create(:group) }
describe '#index' do
+ let(:group) { create(:group) }
+
before do
group.add_owner(user)
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
@@ -86,10 +87,10 @@ describe Groups::GroupMembersController do
context 'when member is not found' do
before { sign_in(user) }
- it 'returns 403' do
+ it 'returns 404' do
delete :leave, group_id: group
- expect(response).to have_http_status(403)
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/controllers/namespaces_controller_spec.rb b/spec/controllers/namespaces_controller_spec.rb
deleted file mode 100644
index 2b334ed1172..00000000000
--- a/spec/controllers/namespaces_controller_spec.rb
+++ /dev/null
@@ -1,118 +0,0 @@
-require 'spec_helper'
-
-describe NamespacesController do
- let!(:user) { create(:user, avatar: fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "image/png")) }
-
- describe "GET show" do
- context "when the namespace belongs to a user" do
- let!(:other_user) { create(:user) }
-
- it "redirects to the user's page" do
- get :show, id: other_user.username
-
- expect(response).to redirect_to(user_path(other_user))
- end
- end
-
- context "when the namespace belongs to a group" do
- let!(:group) { create(:group) }
-
- context "when the group is public" do
- context "when not signed in" do
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
- end
-
- context "when the group is private" do
- before do
- group.update_attribute(:visibility_level, Group::PRIVATE)
- end
-
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, id: group.path
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- context "when the user has access to the group" do
- before do
- group.add_developer(user)
- end
-
- context "when the user is blocked" do
- before do
- user.block
- end
-
- it "redirects to the sign in page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context "when the user isn't blocked" do
- it "redirects to the group's page" do
- get :show, id: group.path
-
- expect(response).to redirect_to(group_path(group))
- end
- end
- end
-
- context "when the user doesn't have access to the group" do
- it "responds with status 404" do
- get :show, id: group.path
-
- expect(response).to have_http_status(404)
- end
- end
- end
- end
- end
-
- context "when the namespace doesn't exist" do
- context "when signed in" do
- before do
- sign_in(user)
- end
-
- it "responds with status 404" do
- get :show, id: "doesntexist"
-
- expect(response).to have_http_status(404)
- end
- end
-
- context "when not signed in" do
- it "redirects to the sign in page" do
- get :show, id: "doesntexist"
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
- end
- end
-end
diff --git a/spec/controllers/projects/blob_controller_spec.rb b/spec/controllers/projects/blob_controller_spec.rb
index 9444a50b1ce..52d13fb6f9e 100644
--- a/spec/controllers/projects/blob_controller_spec.rb
+++ b/spec/controllers/projects/blob_controller_spec.rb
@@ -5,7 +5,6 @@ describe Projects::BlobController do
let(:user) { create(:user) }
before do
- user = create(:user)
project.team << [user, :master]
sign_in(user)
diff --git a/spec/controllers/projects/boards/issues_controller_spec.rb b/spec/controllers/projects/boards/issues_controller_spec.rb
index 2896636db5a..566658b508d 100644
--- a/spec/controllers/projects/boards/issues_controller_spec.rb
+++ b/spec/controllers/projects/boards/issues_controller_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe Projects::Boards::IssuesController do
let(:project) { create(:project_with_board) }
let(:user) { create(:user) }
+ let(:guest) { create(:user) }
let(:planning) { create(:label, project: project, name: 'Planning') }
let(:development) { create(:label, project: project, name: 'Development') }
@@ -12,6 +13,7 @@ describe Projects::Boards::IssuesController do
before do
project.team << [user, :master]
+ project.team << [guest, :guest]
end
describe 'GET index' do
@@ -61,6 +63,60 @@ describe Projects::Boards::IssuesController do
end
end
+ describe 'POST create' do
+ context 'with valid params' do
+ it 'returns a successful 200 response' do
+ create_issue user: user, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(200)
+ end
+
+ it 'returns the created issue' do
+ create_issue user: user, list: list1, title: 'New issue'
+
+ expect(response).to match_response_schema('issue')
+ end
+ end
+
+ context 'with invalid params' do
+ context 'when title is nil' do
+ it 'returns an unprocessable entity 422 response' do
+ create_issue user: user, list: list1, title: nil
+
+ expect(response).to have_http_status(422)
+ end
+ end
+
+ context 'when list does not belongs to project board' do
+ it 'returns a not found 404 response' do
+ list = create(:list)
+
+ create_issue user: user, list: list, title: 'New issue'
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
+ context 'with unauthorized user' do
+ it 'returns a forbidden 403 response' do
+ create_issue user: guest, list: list1, title: 'New issue'
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ def create_issue(user:, list:, title:)
+ sign_in(user)
+
+ post :create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ list_id: list.to_param,
+ issue: { title: title },
+ format: :json
+ end
+ end
+
describe 'PATCH update' do
let(:issue) { create(:labeled_issue, project: project, labels: [planning]) }
@@ -93,13 +149,7 @@ describe Projects::Boards::IssuesController do
end
context 'with unauthorized user' do
- let(:guest) { create(:user) }
-
- before do
- project.team << [guest, :guest]
- end
-
- it 'returns a successful 403 response' do
+ it 'returns a forbidden 403 response' do
move user: guest, issue: issue, from_list_id: list1.id, to_list_id: list2.id
expect(response).to have_http_status(403)
diff --git a/spec/controllers/projects/boards/lists_controller_spec.rb b/spec/controllers/projects/boards/lists_controller_spec.rb
index d687dea3c3b..709006a3601 100644
--- a/spec/controllers/projects/boards/lists_controller_spec.rb
+++ b/spec/controllers/projects/boards/lists_controller_spec.rb
@@ -20,10 +20,7 @@ describe Projects::Boards::ListsController do
end
it 'returns a list of board lists' do
- board = project.create_board
- create(:backlog_list, board: board)
create(:list, board: board)
- create(:done_list, board: board)
read_board_list user: user
diff --git a/spec/controllers/projects/group_links_controller_spec.rb b/spec/controllers/projects/group_links_controller_spec.rb
index fbe8758dda7..b9d9117c928 100644
--- a/spec/controllers/projects/group_links_controller_spec.rb
+++ b/spec/controllers/projects/group_links_controller_spec.rb
@@ -1,8 +1,9 @@
require 'spec_helper'
describe Projects::GroupLinksController do
- let(:project) { create(:project, :private) }
let(:group) { create(:group, :private) }
+ let(:group2) { create(:group, :private) }
+ let(:project) { create(:project, :private, group: group2) }
let(:user) { create(:user) }
before do
@@ -46,5 +47,39 @@ describe Projects::GroupLinksController do
expect(group.shared_projects).not_to include project
end
end
+
+ context 'when project group id equal link group id' do
+ before do
+ post(:create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ link_group_id: group2.id,
+ link_group_access: ProjectGroupLink.default_access)
+ end
+
+ it 'does not share project with selected group' do
+ expect(group2.shared_projects).not_to include project
+ end
+
+ it 'redirects to project group links page' do
+ expect(response).to redirect_to(
+ namespace_project_group_links_path(project.namespace, project)
+ )
+ end
+ end
+
+ context 'when link group id is not present' do
+ before do
+ post(:create, namespace_id: project.namespace.to_param,
+ project_id: project.to_param,
+ link_group_access: ProjectGroupLink.default_access)
+ end
+
+ it 'redirects to project group links page' do
+ expect(response).to redirect_to(
+ namespace_project_group_links_path(project.namespace, project)
+ )
+ expect(flash[:alert]).to eq('Please select a group.')
+ end
+ end
end
end
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 94c9edc91fe..742edd8ba3d 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -644,6 +644,20 @@ describe Projects::MergeRequestsController do
end
end
+ context 'POST remove_wip' do
+ it 'removes the wip status' do
+ merge_request.title = merge_request.wip_title
+ merge_request.save
+
+ post :remove_wip,
+ namespace_id: merge_request.project.namespace.to_param,
+ project_id: merge_request.project.to_param,
+ id: merge_request.iid
+
+ expect(merge_request.reload.title).to eq(merge_request.wipless_title)
+ end
+ end
+
context 'POST resolve_conflicts' do
let(:json_response) { JSON.parse(response.body) }
let!(:original_head_sha) { merge_request_with_conflicts.diff_head_sha }
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index 5e2a8cf3849..074f85157de 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -135,11 +135,11 @@ describe Projects::ProjectMembersController do
context 'when member is not found' do
before { sign_in(user) }
- it 'returns 403' do
+ it 'returns 404' do
delete :leave, namespace_id: project.namespace,
project_id: project
- expect(response).to have_http_status(403)
+ expect(response).to have_http_status(404)
end
end
diff --git a/spec/controllers/projects/repositories_controller_spec.rb b/spec/controllers/projects/repositories_controller_spec.rb
index 2fe3c263524..38e02a46626 100644
--- a/spec/controllers/projects/repositories_controller_spec.rb
+++ b/spec/controllers/projects/repositories_controller_spec.rb
@@ -8,7 +8,7 @@ describe Projects::RepositoriesController do
it 'responds with redirect in correct format' do
get :archive, namespace_id: project.namespace.path, project_id: project.path, format: "zip"
- expect(response.content_type).to start_with 'text/html'
+ expect(response.header["Content-Type"]).to start_with('text/html')
expect(response).to be_redirect
end
end
diff --git a/spec/controllers/projects/templates_controller_spec.rb b/spec/controllers/projects/templates_controller_spec.rb
index 7b3a26d7ca7..19a152bcb05 100644
--- a/spec/controllers/projects/templates_controller_spec.rb
+++ b/spec/controllers/projects/templates_controller_spec.rb
@@ -13,7 +13,7 @@ describe Projects::TemplatesController do
end
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index b0f740f48f7..da0fdce39db 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -63,6 +63,28 @@ describe ProjectsController do
end
end
+ context "project with broken repo" do
+ let(:empty_project) { create(:project_broken_repo, :public) }
+
+ before { sign_in(user) }
+
+ User.project_views.keys.each do |project_view|
+ context "with #{project_view} view set" do
+ before do
+ user.update_attributes(project_view: project_view)
+
+ get :show, namespace_id: empty_project.namespace.path, id: empty_project.path
+ end
+
+ it "renders the empty project view" do
+ allow(Project).to receive(:repo).and_raise(Gitlab::Git::Repository::NoRepository)
+
+ expect(response).to render_template('projects/no_repo')
+ end
+ end
+ end
+ end
+
context "rendering default project view" do
render_views
diff --git a/spec/controllers/sessions_controller_spec.rb b/spec/controllers/sessions_controller_spec.rb
index 8f27e616c3e..48d69377461 100644
--- a/spec/controllers/sessions_controller_spec.rb
+++ b/spec/controllers/sessions_controller_spec.rb
@@ -109,6 +109,44 @@ describe SessionsController do
end
end
+ context 'when the user is on their last attempt' do
+ before do
+ user.update(failed_attempts: User.maximum_attempts.pred)
+ end
+
+ context 'when OTP is valid' do
+ it 'authenticates correctly' do
+ authenticate_2fa(otp_attempt: user.current_otp)
+
+ expect(subject.current_user).to eq user
+ end
+ end
+
+ context 'when OTP is invalid' do
+ before { authenticate_2fa(otp_attempt: 'invalid') }
+
+ it 'does not authenticate' do
+ expect(subject.current_user).not_to eq user
+ end
+
+ it 'warns about invalid login' do
+ expect(response).to set_flash.now[:alert]
+ .to /Invalid Login or password/
+ end
+
+ it 'locks the user' do
+ expect(user.reload).to be_access_locked
+ end
+
+ it 'keeps the user locked on future login attempts' do
+ post(:create, user: { login: user.username, password: user.password })
+
+ expect(response)
+ .to set_flash.now[:alert].to /Invalid Login or password/
+ end
+ end
+ end
+
context 'when another user does not have 2FA enabled' do
let(:another_user) { create(:user) }
diff --git a/spec/controllers/users_controller_spec.rb b/spec/controllers/users_controller_spec.rb
index 54a2d3d9460..19a8b1fe524 100644
--- a/spec/controllers/users_controller_spec.rb
+++ b/spec/controllers/users_controller_spec.rb
@@ -73,8 +73,8 @@ describe UsersController do
end
context 'forked project' do
- let!(:project) { create(:project) }
- let!(:forked_project) { Projects::ForkService.new(project, user).execute }
+ let(:project) { create(:project) }
+ let(:forked_project) { Projects::ForkService.new(project, user).execute }
before do
sign_in(user)
diff --git a/spec/factories/project_members.rb b/spec/factories/project_members.rb
index cf3659ba275..1ddb305a8af 100644
--- a/spec/factories/project_members.rb
+++ b/spec/factories/project_members.rb
@@ -4,24 +4,9 @@ FactoryGirl.define do
project
master
- trait :guest do
- access_level ProjectMember::GUEST
- end
-
- trait :reporter do
- access_level ProjectMember::REPORTER
- end
-
- trait :developer do
- access_level ProjectMember::DEVELOPER
- end
-
- trait :master do
- access_level ProjectMember::MASTER
- end
-
- trait :owner do
- access_level ProjectMember::OWNER
- end
+ trait(:guest) { access_level ProjectMember::GUEST }
+ trait(:reporter) { access_level ProjectMember::REPORTER }
+ trait(:developer) { access_level ProjectMember::DEVELOPER }
+ trait(:master) { access_level ProjectMember::MASTER }
end
end
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index fb84ba07d25..331172445e4 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -9,6 +9,9 @@ FactoryGirl.define do
namespace
creator
+ # Behaves differently to nil due to cache_has_external_issue_tracker
+ has_external_issue_tracker false
+
trait :public do
visibility_level Gitlab::VisibilityLevel::PUBLIC
end
@@ -27,6 +30,14 @@ FactoryGirl.define do
end
end
+ trait :broken_repo do
+ after(:create) do |project|
+ project.create_repository
+
+ FileUtils.rm_r(File.join(project.repository_storage_path, "#{project.path_with_namespace}.git", 'refs'))
+ end
+ end
+
# Nest Project Feature attributes
transient do
wiki_access_level ProjectFeature::ENABLED
@@ -56,6 +67,13 @@ FactoryGirl.define do
empty_repo
end
+ # Project with broken repository
+ #
+ # Project with an invalid repository state
+ factory :project_broken_repo, parent: :empty_project do
+ broken_repo
+ end
+
# Project with test repository
#
# Test repository source can be found at
@@ -77,6 +95,8 @@ FactoryGirl.define do
end
factory :redmine_project, parent: :project do
+ has_external_issue_tracker true
+
after :create do |project|
project.create_redmine_service(
active: true,
@@ -90,6 +110,8 @@ FactoryGirl.define do
end
factory :jira_project, parent: :project do
+ has_external_issue_tracker true
+
after :create do |project|
project.create_jira_service(
active: true,
@@ -106,6 +128,8 @@ FactoryGirl.define do
factory :project_with_board, parent: :empty_project do
after(:create) do |project|
project.create_board
+ project.board.lists.create(list_type: :backlog)
+ project.board.lists.create(list_type: :done)
end
end
end
diff --git a/spec/features/boards/boards_spec.rb b/spec/features/boards/boards_spec.rb
index 19941978c5f..470e2bdbb9b 100644
--- a/spec/features/boards/boards_spec.rb
+++ b/spec/features/boards/boards_spec.rb
@@ -4,15 +4,11 @@ describe 'Issue Boards', feature: true, js: true do
include WaitForAjax
include WaitForVueResource
- let(:project) { create(:empty_project, :public) }
+ let(:project) { create(:project_with_board, :public) }
let(:user) { create(:user) }
let!(:user2) { create(:user) }
before do
- project.create_board
- project.board.lists.create(list_type: :backlog)
- project.board.lists.create(list_type: :done)
-
project.team << [user, :master]
project.team << [user2, :master]
@@ -38,14 +34,14 @@ describe 'Issue Boards', feature: true, js: true do
end
it 'creates default lists' do
- lists = ['Backlog', 'Development', 'Testing', 'Production', 'Ready', 'Done']
+ lists = ['Backlog', 'To Do', 'Doing', 'Done']
page.within(find('.board-blank-state')) do
click_button('Add default lists')
end
wait_for_vue_resource
- expect(page).to have_selector('.board', count: 6)
+ expect(page).to have_selector('.board', count: 4)
page.all('.board').each_with_index do |list, i|
expect(list.find('.board-title')).to have_content(lists[i])
@@ -62,6 +58,7 @@ describe 'Issue Boards', feature: true, js: true do
let(:bug) { create(:label, project: project, name: 'Bug') }
let!(:backlog) { create(:label, project: project, name: 'Backlog') }
let!(:done) { create(:label, project: project, name: 'Done') }
+ let!(:accepting) { create(:label, project: project, name: 'Accepting Merge Requests') }
let!(:list1) { create(:list, board: project.board, label: planning, position: 0) }
let!(:list2) { create(:list, board: project.board, label: development, position: 1) }
@@ -75,7 +72,7 @@ describe 'Issue Boards', feature: true, js: true do
let!(:issue6) { create(:labeled_issue, project: project, labels: [planning, development]) }
let!(:issue7) { create(:labeled_issue, project: project, labels: [development]) }
let!(:issue8) { create(:closed_issue, project: project) }
- let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug]) }
+ let!(:issue9) { create(:labeled_issue, project: project, labels: [testing, bug, accepting]) }
before do
visit namespace_project_board_path(project.namespace, project)
@@ -441,6 +438,57 @@ describe 'Issue Boards', feature: true, js: true do
wait_for_empty_boards((2..4))
end
+ it 'filters by label with space after reload' do
+ page.within '.issues-filters' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(accepting.title)
+ wait_for_vue_resource(spinner: false)
+ find('.dropdown-menu-close').click
+ end
+ end
+
+ # Test after reload
+ page.evaluate_script 'window.location.reload()'
+
+ wait_for_vue_resource
+
+ page.within(find('.board', match: :first)) do
+ expect(page.find('.board-header')).to have_content('1')
+ expect(page).to have_selector('.card', count: 1)
+ end
+
+ page.within(find('.board:nth-child(2)')) do
+ expect(page.find('.board-header')).to have_content('0')
+ expect(page).to have_selector('.card', count: 0)
+ end
+ end
+
+ it 'removes filtered labels' do
+ wait_for_vue_resource
+
+ page.within '.labels-filter' do
+ click_button('Label')
+ wait_for_ajax
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ expect(page).to have_css('input[name="label_name[]"]', visible: false)
+
+ page.within '.dropdown-menu-labels' do
+ click_link(testing.title)
+ wait_for_vue_resource(spinner: false)
+ end
+
+ expect(page).not_to have_css('input[name="label_name[]"]', visible: false)
+ end
+ end
+
it 'infinite scrolls list with label filter' do
50.times do
create(:labeled_issue, project: project, labels: [testing])
diff --git a/spec/features/boards/new_issue_spec.rb b/spec/features/boards/new_issue_spec.rb
new file mode 100644
index 00000000000..c046e6b8d79
--- /dev/null
+++ b/spec/features/boards/new_issue_spec.rb
@@ -0,0 +1,80 @@
+require 'rails_helper'
+
+describe 'Issue Boards new issue', feature: true, js: true do
+ include WaitForAjax
+ include WaitForVueResource
+
+ let(:project) { create(:project_with_board, :public) }
+ let(:user) { create(:user) }
+
+ context 'authorized user' do
+ before do
+ project.team << [user, :master]
+
+ login_as(user)
+
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+
+ expect(page).to have_selector('.board', count: 3)
+ end
+
+ it 'displays new issue button' do
+ expect(page).to have_selector('.board-issue-count-holder .btn', count: 1)
+ end
+
+ it 'does not display new issue button in done list' do
+ page.within('.board:nth-child(3)') do
+ expect(page).not_to have_selector('.board-issue-count-holder .btn')
+ end
+ end
+
+ it 'shows form when clicking button' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+
+ expect(page).to have_selector('.board-new-issue-form')
+ end
+ end
+
+ it 'hides form when clicking cancel' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+
+ expect(page).to have_selector('.board-new-issue-form')
+
+ click_button 'Cancel'
+
+ expect(page).to have_selector('.board-new-issue-form', visible: false)
+ end
+ end
+
+ it 'creates new issue' do
+ page.within(first('.board')) do
+ find('.board-issue-count-holder .btn').click
+ end
+
+ page.within(first('.board-new-issue-form')) do
+ find('.form-control').set('bug')
+ click_button 'Submit issue'
+ end
+
+ wait_for_vue_resource
+
+ page.within(first('.board .board-issue-count')) do
+ expect(page).to have_content('1')
+ end
+ end
+ end
+
+ context 'unauthorized user' do
+ before do
+ visit namespace_project_board_path(project.namespace, project)
+ wait_for_vue_resource
+ end
+
+ it 'does not display new issue button' do
+ expect(page).to have_selector('.board-issue-count-holder .btn', count: 0)
+ end
+ end
+end
diff --git a/spec/features/calendar_spec.rb b/spec/features/calendar_spec.rb
index fd5fbaf2af4..7fa0c95cae2 100644
--- a/spec/features/calendar_spec.rb
+++ b/spec/features/calendar_spec.rb
@@ -5,13 +5,43 @@ feature 'Contributions Calendar', js: true, feature: true do
let(:contributed_project) { create(:project, :public) }
- before do
- login_as :user
+ # Ex/ Sunday Jan 1, 2016
+ date_format = '%A %b %-d, %Y'
+
+ issue_title = 'Bug in old browser'
+ issue_params = { title: issue_title }
+
+ def get_cell_color_selector(contributions)
+ contribution_cell = '.user-contrib-cell'
+ activity_colors = Array['#ededed', '#acd5f2', '#7fa8c9', '#527ba0', '#254e77']
+ activity_colors_index = 0
+
+ if contributions > 0 && contributions < 10
+ activity_colors_index = 1
+ elsif contributions >= 10 && contributions < 20
+ activity_colors_index = 2
+ elsif contributions >= 20 && contributions < 30
+ activity_colors_index = 3
+ elsif contributions >= 30
+ activity_colors_index = 4
+ end
+
+ "#{contribution_cell}[fill='#{activity_colors[activity_colors_index]}']"
+ end
- issue_params = { title: 'Bug in old browser' }
- Issues::CreateService.new(contributed_project, @user, issue_params).execute
+ def get_cell_date_selector(contributions, date)
+ contribution_text = 'No contributions'
- # Push code contribution
+ if contributions === 1
+ contribution_text = '1 contribution'
+ elsif contributions > 1
+ contribution_text = "#{contributions} contributions"
+ end
+
+ "#{get_cell_color_selector(contributions)}[data-original-title='#{contribution_text}<br />#{date}']"
+ end
+
+ def push_code_contribution
push_params = {
project: contributed_project,
action: Event::PUSHED,
@@ -20,7 +50,10 @@ feature 'Contributions Calendar', js: true, feature: true do
}
Event.create(push_params)
+ end
+ before do
+ login_as :user
visit @user.username
wait_for_ajax
end
@@ -29,11 +62,71 @@ feature 'Contributions Calendar', js: true, feature: true do
expect(page).to have_css('.js-contrib-calendar')
end
- it 'displays calendar activity log', js: true do
- expect(find('.content_list .event-note')).to have_content "Bug in old browser"
+ describe '1 calendar activity' do
+ before do
+ Issues::CreateService.new(contributed_project, @user, issue_params).execute
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar activity log', js: true do
+ expect(find('.content_list .event-note')).to have_content issue_title
+ end
+
+ it 'displays calendar activity square color for 1 contribution', js: true do
+ expect(page).to have_selector(get_cell_color_selector(1), count: 1)
+ end
+
+ it 'displays calendar activity square on the correct date', js: true do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
end
- it 'displays calendar activity square color', js: true do
- expect(page).to have_selector('.user-contrib-cell[fill=\'#acd5f2\']', count: 1)
+ describe '10 calendar activities' do
+ before do
+ (0..9).each do |i|
+ push_code_contribution()
+ end
+
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar activity square color for 10 contributions', js: true do
+ expect(page).to have_selector(get_cell_color_selector(10), count: 1)
+ end
+
+ it 'displays calendar activity square on the correct date', js: true do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(10, today), count: 1)
+ end
+ end
+
+ describe 'calendar activity on two days' do
+ before do
+ push_code_contribution()
+
+ Timecop.freeze(Date.yesterday)
+ Issues::CreateService.new(contributed_project, @user, issue_params).execute
+ Timecop.return
+
+ visit @user.username
+ wait_for_ajax
+ end
+
+ it 'displays calendar activity squares for both days', js: true do
+ expect(page).to have_selector(get_cell_color_selector(1), count: 2)
+ end
+
+ it 'displays calendar activity square for yesterday', js: true do
+ yesterday = Date.yesterday.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, yesterday), count: 1)
+ end
+
+ it 'displays calendar activity square for today', js: true do
+ today = Date.today.strftime(date_format)
+ expect(page).to have_selector(get_cell_date_selector(1, today), count: 1)
+ end
end
end
diff --git a/spec/features/compare_spec.rb b/spec/features/compare_spec.rb
index ca7f73e24cc..33dfd0d5b62 100644
--- a/spec/features/compare_spec.rb
+++ b/spec/features/compare_spec.rb
@@ -12,15 +12,16 @@ describe "Compare", js: true do
describe "branches" do
it "pre-populates fields" do
- expect(page.find_field("from").value).to eq("master")
+ expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("master")
+ expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("master")
end
it "compares branches" do
- fill_in "from", with: "fea"
- find("#from").click
+ select_using_dropdown "from", "feature"
+ expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("feature")
- click_link "feature"
- expect(page.find_field("from").value).to eq("feature")
+ select_using_dropdown "to", "binary-encoding"
+ expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("binary-encoding")
click_button "Compare"
expect(page).to have_content "Commits"
@@ -29,14 +30,21 @@ describe "Compare", js: true do
describe "tags" do
it "compares tags" do
- fill_in "from", with: "v1.0"
- find("#from").click
+ select_using_dropdown "from", "v1.0.0"
+ expect(find(".js-compare-from-dropdown .dropdown-toggle-text")).to have_content("v1.0.0")
- click_link "v1.0.0"
- expect(page.find_field("from").value).to eq("v1.0.0")
+ select_using_dropdown "to", "v1.1.0"
+ expect(find(".js-compare-to-dropdown .dropdown-toggle-text")).to have_content("v1.1.0")
click_button "Compare"
expect(page).to have_content "Commits"
end
end
+
+ def select_using_dropdown(dropdown_type, selection)
+ dropdown = find(".js-compare-#{dropdown_type}-dropdown")
+ dropdown.find(".compare-dropdown-toggle").click
+ dropdown.fill_in("Filter by branch/tag", with: selection)
+ click_link selection
+ end
end
diff --git a/spec/features/dashboard/snippets_spec.rb b/spec/features/dashboard/snippets_spec.rb
new file mode 100644
index 00000000000..62937688c22
--- /dev/null
+++ b/spec/features/dashboard/snippets_spec.rb
@@ -0,0 +1,15 @@
+require 'spec_helper'
+
+describe 'Dashboard snippets', feature: true do
+ context 'when the project has snippets' do
+ let(:project) { create(:empty_project, :public) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ login_as(project.owner)
+ visit dashboard_snippets_path
+ end
+
+ it_behaves_like 'paginated snippets'
+ end
+end
diff --git a/spec/features/dashboard_issues_spec.rb b/spec/features/dashboard_issues_spec.rb
index 3fb1cb37544..9b54b5301e5 100644
--- a/spec/features/dashboard_issues_spec.rb
+++ b/spec/features/dashboard_issues_spec.rb
@@ -21,6 +21,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link 'No Milestone'
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
@@ -29,6 +30,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link 'Any Milestone'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
expect(page).to have_selector('.issue', count: 2)
end
@@ -39,6 +41,7 @@ describe "Dashboard Issues filtering", feature: true, js: true do
click_link milestone.title
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_selector('.issue', count: 1)
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 4309a726917..68ea4eeae31 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -44,6 +44,10 @@ feature 'Environments', feature: true do
scenario 'does show deployment SHA' do
expect(page).to have_link(deployment.short_sha)
end
+
+ scenario 'does show deployment internal id' do
+ expect(page).to have_content(deployment.iid)
+ end
context 'with build and manual actions' do
given(:pipeline) { create(:ci_pipeline, project: project) }
@@ -61,6 +65,20 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
+
+ scenario 'does show build name and id' do
+ expect(page).to have_link("#{build.name} (##{build.id})")
+ end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
end
end
@@ -122,6 +140,16 @@ feature 'Environments', feature: true do
expect(page).to have_content(manual.name)
expect(manual.reload).to be_pending
end
+
+ context 'with external_url' do
+ given(:environment) { create(:environment, project: project, external_url: 'https://git.gitlab.com') }
+ given(:build) { create(:ci_build, pipeline: pipeline) }
+ given(:deployment) { create(:deployment, environment: environment, deployable: build) }
+
+ scenario 'does show an external link button' do
+ expect(page).to have_link(nil, href: environment.external_url)
+ end
+ end
end
end
end
diff --git a/spec/features/expand_collapse_diffs_spec.rb b/spec/features/expand_collapse_diffs_spec.rb
index 8863554ee91..6c938bdead8 100644
--- a/spec/features/expand_collapse_diffs_spec.rb
+++ b/spec/features/expand_collapse_diffs_spec.rb
@@ -68,7 +68,7 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a diff for a renamed file' do
before do
- large_diff_renamed.find('.nothing-here-block').click
+ large_diff_renamed.find('.click-to-expand').click
wait_for_ajax
end
@@ -87,7 +87,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding a large diff' do
before do
- click_link('large_diff.md')
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `large_diff.md` title
+ all('.file-title')[1].click
wait_for_ajax
end
@@ -128,7 +131,10 @@ feature 'Expand and collapse diffs', js: true, feature: true do
context 'expanding the diff' do
before do
- click_link('large_diff.md')
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `large_diff.md` title
+ all('.file-title')[1].click
wait_for_ajax
end
@@ -146,7 +152,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 'collapsing an expanded diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code')
@@ -154,7 +165,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 're-expanding the same diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'shows the diff content' do
expect(small_diff).to have_selector('.code')
@@ -231,7 +247,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 'collapsing an expanded diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'hides the diff content' do
expect(small_diff).not_to have_selector('.code')
@@ -239,7 +260,12 @@ feature 'Expand and collapse diffs', js: true, feature: true do
end
context 're-expanding the same diff' do
- before { click_link('small_diff.md') }
+ before do
+ # Wait for diffs
+ find('.file-title', match: :first)
+ # Click `small_diff.md` title
+ all('.file-title')[3].click
+ end
it 'shows the diff content' do
expect(small_diff).to have_selector('.code')
diff --git a/spec/features/groups_spec.rb b/spec/features/groups_spec.rb
index 2d8b59472e8..c54ec2563ad 100644
--- a/spec/features/groups_spec.rb
+++ b/spec/features/groups_spec.rb
@@ -5,6 +5,12 @@ feature 'Group', feature: true do
login_as(:admin)
end
+ matcher :have_namespace_error_message do
+ match do |page|
+ page.has_content?("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.', '.git' or '.atom'.")
+ end
+ end
+
describe 'creating a group with space in group path' do
it 'renders new group form with validation errors' do
visit new_group_path
@@ -13,7 +19,31 @@ feature 'Group', feature: true do
click_button 'Create group'
expect(current_path).to eq(groups_path)
- expect(page).to have_content("Path can contain only letters, digits, '_', '-' and '.'. Cannot start with '-' or end in '.'.")
+ expect(page).to have_namespace_error_message
+ end
+ end
+
+ describe 'creating a group with .atom at end of group path' do
+ it 'renders new group form with validation errors' do
+ visit new_group_path
+ fill_in 'Group path', with: 'atom_group.atom'
+
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_namespace_error_message
+ end
+ end
+
+ describe 'creating a group with .git at end of group path' do
+ it 'renders new group form with validation errors' do
+ visit new_group_path
+ fill_in 'Group path', with: 'git_group.git'
+
+ click_button 'Create group'
+
+ expect(current_path).to eq(groups_path)
+ expect(page).to have_namespace_error_message
end
end
diff --git a/spec/features/issues/filter_by_labels_spec.rb b/spec/features/issues/filter_by_labels_spec.rb
index 908b18e5339..0253629f753 100644
--- a/spec/features/issues/filter_by_labels_spec.rb
+++ b/spec/features/issues/filter_by_labels_spec.rb
@@ -1,10 +1,10 @@
require 'rails_helper'
-feature 'Issue filtering by Labels', feature: true do
+feature 'Issue filtering by Labels', feature: true, js: true do
include WaitForAjax
let(:project) { create(:project, :public) }
- let!(:user) { create(:user)}
+ let!(:user) { create(:user) }
let!(:label) { create(:label, project: project) }
before do
@@ -28,156 +28,81 @@ feature 'Issue filtering by Labels', feature: true do
visit namespace_project_issues_path(project.namespace, project)
end
- context 'filter by label bug', js: true do
+ context 'filter by label bug' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('bug')
end
- it 'shows issue "Bugfix1" and "Bugfix2" in issues list' do
+ it 'apply the filter' do
expect(page).to have_content "Bugfix1"
expect(page).to have_content "Bugfix2"
- end
-
- it 'does not show "Feature1" in issues list' do
expect(page).not_to have_content "Feature1"
- end
-
- it 'shows label "bug" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "bug"
- end
-
- it 'does not show label "feature" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
expect(find('.filtered-labels')).not_to have_content "enhancement"
- end
- it 'removes label "bug"' do
find('.js-label-filter-remove').click
wait_for_ajax
expect(find('.filtered-labels', visible: false)).to have_no_content "bug"
end
end
- context 'filter by label feature', js: true do
+ context 'filter by label feature' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('feature')
end
- it 'shows issue "Feature1" in issues list' do
+ it 'applies the filter' do
expect(page).to have_content "Feature1"
- end
-
- it 'does not show "Bugfix1" and "Bugfix2" in issues list' do
expect(page).not_to have_content "Bugfix2"
expect(page).not_to have_content "Bugfix1"
- end
-
- it 'shows label "feature" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "feature"
- end
-
- it 'does not show label "bug" and "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "enhancement"
end
end
- context 'filter by label enhancement', js: true do
+ context 'filter by label enhancement' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('enhancement')
end
- it 'shows issue "Bugfix2" in issues list' do
+ it 'applies the filter' do
expect(page).to have_content "Bugfix2"
- end
-
- it 'does not show "Feature1" and "Bugfix1" in issues list' do
expect(page).not_to have_content "Feature1"
expect(page).not_to have_content "Bugfix1"
- end
-
- it 'shows label "enhancement" in filtered-labels' do
expect(find('.filtered-labels')).to have_content "enhancement"
- end
-
- it 'does not show label "feature" and "bug" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
- context 'filter by label enhancement or feature', js: true do
+ context 'filter by label enhancement and bug in issues list' do
before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
- execute_script("$('.dropdown-menu-labels li:contains(\"feature\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
+ select_labels('bug', 'enhancement')
end
- it 'does not show "Bugfix1" or "Feature1" in issues list' do
- expect(page).not_to have_content "Bugfix1"
+ it 'applies the filters' do
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
+ expect(page).to have_content "Bugfix2"
expect(page).not_to have_content "Feature1"
- end
-
- it 'shows label "enhancement" and "feature" in filtered-labels' do
+ expect(find('.filtered-labels')).to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
- expect(find('.filtered-labels')).to have_content "feature"
- end
-
- it 'does not show label "bug" in filtered-labels' do
- expect(find('.filtered-labels')).not_to have_content "bug"
- end
+ expect(find('.filtered-labels')).not_to have_content "feature"
- it 'removes label "enhancement"' do
find('.js-label-filter-remove', match: :first).click
wait_for_ajax
- expect(find('.filtered-labels')).to have_no_content "enhancement"
- end
- end
-
- context 'filter by label enhancement and bug in issues list', js: true do
- before do
- page.find('.js-label-select').click
- wait_for_ajax
- execute_script("$('.dropdown-menu-labels li:contains(\"enhancement\") a').click()")
- execute_script("$('.dropdown-menu-labels li:contains(\"bug\") a').click()")
- page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
- wait_for_ajax
- end
- it 'shows issue "Bugfix2" in issues list' do
expect(page).to have_content "Bugfix2"
- end
-
- it 'does not show "Feature1"' do
expect(page).not_to have_content "Feature1"
- end
-
- it 'shows label "bug" and "enhancement" in filtered-labels' do
- expect(find('.filtered-labels')).to have_content "bug"
+ expect(page).not_to have_content "Bugfix1"
+ expect(find('.filtered-labels')).not_to have_content "bug"
expect(find('.filtered-labels')).to have_content "enhancement"
- end
-
- it 'does not show label "feature" in filtered-labels' do
expect(find('.filtered-labels')).not_to have_content "feature"
end
end
- context 'remove filtered labels', js: true do
+ context 'remove filtered labels' do
before do
page.within '.labels-filter' do
click_button 'Label'
@@ -200,7 +125,7 @@ feature 'Issue filtering by Labels', feature: true do
end
end
- context 'dropdown filtering', js: true do
+ context 'dropdown filtering' do
it 'filters by label name' do
page.within '.labels-filter' do
click_button 'Label'
@@ -214,4 +139,14 @@ feature 'Issue filtering by Labels', feature: true do
end
end
end
+
+ def select_labels(*labels)
+ page.find('.js-label-select').click
+ wait_for_ajax
+ labels.each do |label|
+ execute_script("$('.dropdown-menu-labels li:contains(\"#{label}\") a').click()")
+ end
+ page.first('.labels-filter .dropdown-title .dropdown-menu-close-icon').click
+ wait_for_ajax
+ end
end
diff --git a/spec/features/issues/filter_issues_spec.rb b/spec/features/issues/filter_issues_spec.rb
index d1501c9791a..78208aed46d 100644
--- a/spec/features/issues/filter_issues_spec.rb
+++ b/spec/features/issues/filter_issues_spec.rb
@@ -7,15 +7,15 @@ describe 'Filter issues', feature: true do
let!(:user) { create(:user)}
let!(:milestone) { create(:milestone, project: project) }
let!(:label) { create(:label, project: project) }
- let!(:issue1) { create(:issue, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
before do
project.team << [user, :master]
login_as(user)
+ create(:issue, project: project)
end
- describe 'Filter issues for assignee from issues#index' do
+ describe 'for assignee from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -45,7 +45,7 @@ describe 'Filter issues', feature: true do
end
end
- describe 'Filter issues for milestone from issues#index' do
+ describe 'for milestone from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -75,7 +75,7 @@ describe 'Filter issues', feature: true do
end
end
- describe 'Filter issues for label from issues#index', js: true do
+ describe 'for label from issues#index', js: true do
before do
visit namespace_project_issues_path(project.namespace, project)
find('.js-label-select').click
@@ -96,9 +96,9 @@ describe 'Filter issues', feature: true do
wait_for_ajax
page.within '.labels-filter' do
- expect(page).to have_content 'No Label'
+ expect(page).to have_content 'Labels'
end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content('No Label')
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content('Labels')
end
it 'filters by a label' do
@@ -110,29 +110,37 @@ describe 'Filter issues', feature: true do
end
it "filters by `won't fix` and another label" do
- find('.dropdown-menu-labels a', text: label.title).click
page.within '.labels-filter' do
- expect(page).to have_content wontfix.title
click_link wontfix.title
+ expect(page).to have_content wontfix.title
+ click_link label.title
end
- expect(find('.js-label-select .dropdown-toggle-text')).to have_content(wontfix.title)
+
+ expect(find('.js-label-select .dropdown-toggle-text')).to have_content("#{wontfix.title} +1 more")
end
it "filters by `won't fix` label followed by another label after page load" do
- find('.dropdown-menu-labels a', text: wontfix.title).click
- # Close label dropdown to load
+ page.within '.labels-filter' do
+ click_link wontfix.title
+ expect(page).to have_content wontfix.title
+ end
+
find('body').click
+
expect(find('.filtered-labels')).to have_content(wontfix.title)
find('.js-label-select').click
wait_for_ajax
find('.dropdown-menu-labels a', text: label.title).click
- # Close label dropdown to load
+
find('body').click
+
+ expect(find('.filtered-labels')).to have_content(wontfix.title)
expect(find('.filtered-labels')).to have_content(label.title)
find('.js-label-select').click
wait_for_ajax
+
expect(find('.dropdown-menu-labels li', text: wontfix.title)).to have_css('.is-active')
expect(find('.dropdown-menu-labels li', text: label.title)).to have_css('.is-active')
end
@@ -146,7 +154,7 @@ describe 'Filter issues', feature: true do
end
end
- describe 'Filter issues for assignee and label from issues#index' do
+ describe 'for assignee and label from issues#index' do
before do
visit namespace_project_issues_path(project.namespace, project)
@@ -226,6 +234,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and label' do
fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -236,6 +245,7 @@ describe 'Filter issues', feature: true do
end
find('.dropdown-menu-close-icon').click
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
@@ -244,6 +254,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and milestone' do
fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -253,6 +264,7 @@ describe 'Filter issues', feature: true do
click_link '8'
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
@@ -261,6 +273,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and assignee' do
fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -270,6 +283,7 @@ describe 'Filter issues', feature: true do
click_link user.name
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
@@ -278,6 +292,7 @@ describe 'Filter issues', feature: true do
it 'filters by text and author' do
fill_in 'issuable_search', with: 'Bug'
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
@@ -287,6 +302,7 @@ describe 'Filter issues', feature: true do
click_link user.name
end
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 1)
end
@@ -315,6 +331,7 @@ describe 'Filter issues', feature: true do
find('.dropdown-menu-close-icon').click
wait_for_ajax
+ expect(page).to have_issuable_counts(open: 2, closed: 0, all: 2)
page.within '.issues-list' do
expect(page).to have_selector('.issue', count: 2)
end
diff --git a/spec/features/issues/form_spec.rb b/spec/features/issues/form_spec.rb
new file mode 100644
index 00000000000..8771cc8e157
--- /dev/null
+++ b/spec/features/issues/form_spec.rb
@@ -0,0 +1,119 @@
+require 'rails_helper'
+
+describe 'New/edit issue', feature: true, js: true do
+ let!(:project) { create(:project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+ let!(:issue) { create(:issue, project: project, assignee: user, milestone: milestone) }
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'new issue' do
+ before do
+ visit new_namespace_project_issue_path(project.namespace, project)
+ end
+
+ it 'allows user to create new issue' do
+ fill_in 'issue_title', with: 'title'
+ fill_in 'issue_description', with: 'title'
+
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Submit issue'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+
+ context 'edit issue' do
+ before do
+ visit edit_namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'allows user to update issue' do
+ expect(find('input[name="issue[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ expect(find('input[name="issue[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+
+ page.within '.js-user-search' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="issue[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/move_spec.rb b/spec/features/issues/move_spec.rb
index 7773c486b4e..055210399a7 100644
--- a/spec/features/issues/move_spec.rb
+++ b/spec/features/issues/move_spec.rb
@@ -55,7 +55,7 @@ feature 'issue move to another project' do
first('.select2-choice').click
end
- fill_in('s2id_autogen2_search', with: new_project_search.name)
+ fill_in('s2id_autogen1_search', with: new_project_search.name)
page.within '.select2-drop' do
expect(page).to have_content(new_project_search.name)
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 105629c485a..3f2da1c380c 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -25,32 +25,88 @@ feature 'Issues > User uses slash commands', feature: true, js: true do
describe 'adding a due date from note' do
let(:issue) { create(:issue, project: project) }
- it 'does not create a note, and sets the due date accordingly' do
- write_note("/due 2016-08-28")
+ context 'when the current user can update the due date' do
+ it 'does not create a note, and sets the due date accordingly' do
+ write_note("/due 2016-08-28")
- expect(page).not_to have_content '/due 2016-08-28'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).to have_content 'Your commands have been executed!'
- issue.reload
+ issue.reload
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ end
+ end
+
+ context 'when the current user cannot update the due date' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ 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 'Your commands have been executed!'
+
+ issue.reload
+
+ expect(issue.due_date).to be_nil
+ end
end
end
describe 'removing a due date from note' do
let(:issue) { create(:issue, project: project, due_date: Date.new(2016, 8, 28)) }
- it 'does not create a note, and removes the due date accordingly' do
- expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ context 'when the current user can update the due date' do
+ it 'does not create a note, and removes the due date accordingly' do
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+
+ write_note("/remove_due_date")
+
+ expect(page).not_to have_content '/remove_due_date'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ issue.reload
- write_note("/remove_due_date")
+ expect(issue.due_date).to be_nil
+ end
+ end
+
+ context 'when the current user cannot update the due date' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ 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 'Your commands have been executed!'
+
+ issue.reload
- expect(page).not_to have_content '/remove_due_date'
- expect(page).to have_content 'Your commands have been executed!'
+ expect(issue.due_date).to eq Date.new(2016, 8, 28)
+ end
+ end
+ end
+
+ describe 'toggling the WIP prefix from the title from note' do
+ let(:issue) { create(:issue, project: project) }
- issue.reload
+ it 'does not recognize the command nor create a note' do
+ write_note("/wip")
- expect(issue.due_date).to be_nil
+ expect(page).not_to have_content '/wip'
end
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index 22359c8f938..b504329656f 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -51,9 +51,8 @@ describe 'Issues', feature: true do
expect(page).to have_content "Assignee #{@user.name}"
- first('#s2id_issue_assignee_id').click
- sleep 2 # wait for ajax stuff to complete
- first('.user-result').click
+ first('.js-user-search').click
+ click_link 'Unassigned'
click_button 'Save changes'
@@ -369,6 +368,24 @@ describe 'Issues', feature: true do
end
end
+ describe 'update labels from issue#show', js: true do
+ let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
+ let!(:label) { create(:label, project: project) }
+
+ before do
+ visit namespace_project_issue_path(project.namespace, project, issue)
+ end
+
+ it 'will not send ajax request when no data is changed' do
+ page.within '.labels' do
+ click_link 'Edit'
+ first('.dropdown-menu-close').click
+
+ expect(page).not_to have_selector('.block-loading')
+ end
+ end
+ end
+
describe 'update assignee from issue#show' do
let(:issue) { create(:issue, project: project, author: @user, assignee: @user) }
diff --git a/spec/features/merge_requests/filter_by_milestone_spec.rb b/spec/features/merge_requests/filter_by_milestone_spec.rb
index bb0bb590a46..d917d5950ec 100644
--- a/spec/features/merge_requests/filter_by_milestone_spec.rb
+++ b/spec/features/merge_requests/filter_by_milestone_spec.rb
@@ -17,6 +17,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(Milestone::None.title)
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
@@ -39,6 +40,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(Milestone::Upcoming.title)
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
@@ -61,6 +63,7 @@ feature 'Merge Request filtering by Milestone', feature: true do
visit_merge_requests(project)
filter_by_milestone(milestone.title)
+ expect(page).to have_issuable_counts(open: 1, closed: 0, all: 1)
expect(page).to have_css('.merge-request', count: 1)
end
diff --git a/spec/features/merge_requests/form_spec.rb b/spec/features/merge_requests/form_spec.rb
new file mode 100644
index 00000000000..7594cbf54e8
--- /dev/null
+++ b/spec/features/merge_requests/form_spec.rb
@@ -0,0 +1,273 @@
+require 'rails_helper'
+
+describe 'New/edit merge request', feature: true, js: true do
+ let!(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
+ let(:fork_project) { create(:project, forked_from_project: project) }
+ let!(:user) { create(:user)}
+ let!(:milestone) { create(:milestone, project: project) }
+ let!(:label) { create(:label, project: project) }
+ let!(:label2) { create(:label, project: project) }
+
+ before do
+ project.team << [user, :master]
+ end
+
+ context 'owned projects' do
+ before do
+ login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request: {
+ source_project_id: project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+
+ it 'creates new merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Submit merge request'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+
+ context 'edit merge request' do
+ before do
+ merge_request = create(:merge_request,
+ source_project: project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master'
+ )
+
+ visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'updates merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+ end
+
+ context 'forked project' do
+ before do
+ fork_project.team << [user, :master]
+ login_as(user)
+ end
+
+ context 'new merge request' do
+ before do
+ visit new_namespace_project_merge_request_path(
+ fork_project.namespace,
+ fork_project,
+ merge_request: {
+ source_project_id: fork_project.id,
+ target_project_id: project.id,
+ source_branch: 'fix',
+ target_branch: 'master'
+ })
+ end
+
+ it 'creates new merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+
+ click_button 'Submit merge request'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+
+ context 'edit merge request' do
+ before do
+ merge_request = create(:merge_request,
+ source_project: fork_project,
+ target_project: project,
+ source_branch: 'fix',
+ target_branch: 'master'
+ )
+
+ visit edit_namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'should update merge request' do
+ click_button 'Assignee'
+ page.within '.dropdown-menu-user' do
+ click_link user.name
+ end
+ expect(find('input[name="merge_request[assignee_id]"]', visible: false).value).to match(user.id.to_s)
+ page.within '.js-assignee-search' do
+ expect(page).to have_content user.name
+ end
+
+ click_button 'Milestone'
+ page.within '.issue-milestone' do
+ click_link milestone.title
+ end
+ expect(find('input[name="merge_request[milestone_id]"]', visible: false).value).to match(milestone.id.to_s)
+ page.within '.js-milestone-select' do
+ expect(page).to have_content milestone.title
+ end
+
+ click_button 'Labels'
+ page.within '.dropdown-menu-labels' do
+ click_link label.title
+ click_link label2.title
+ end
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[1].value).to match(label.id.to_s)
+ expect(page.all('input[name="merge_request[label_ids][]"]', visible: false)[2].value).to match(label2.id.to_s)
+ page.within '.js-label-select' do
+ expect(page).to have_content label.title
+ end
+
+ click_button 'Save changes'
+
+ page.within '.issuable-sidebar' do
+ page.within '.assignee' do
+ expect(page).to have_content user.name
+ end
+
+ page.within '.milestone' do
+ expect(page).to have_content milestone.title
+ end
+
+ page.within '.labels' do
+ expect(page).to have_content label.title
+ expect(page).to have_content label2.title
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/merge_requests/merge_request_versions_spec.rb b/spec/features/merge_requests/merge_request_versions_spec.rb
index 22d9e42119d..23cee891bac 100644
--- a/spec/features/merge_requests/merge_request_versions_spec.rb
+++ b/spec/features/merge_requests/merge_request_versions_spec.rb
@@ -1,12 +1,13 @@
require 'spec_helper'
feature 'Merge Request versions', js: true, feature: true do
+ let(:merge_request) { create(:merge_request, importing: true) }
+ let(:project) { merge_request.source_project }
+
before do
login_as :admin
- merge_request = create(:merge_request, importing: true)
merge_request.merge_request_diffs.create(head_commit_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
merge_request.merge_request_diffs.create(head_commit_sha: '5937ac0a7beb003549fc5fd26fc247adbce4a52e')
- project = merge_request.source_project
visit diffs_namespace_project_merge_request_path(project.namespace, project, merge_request)
end
@@ -47,6 +48,16 @@ feature 'Merge Request versions', js: true, feature: true do
end
end
+ it 'has a path with comparison context' do
+ expect(page).to have_current_path diffs_namespace_project_merge_request_path(
+ project.namespace,
+ project,
+ merge_request.iid,
+ diff_id: 2,
+ start_sha: '6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9'
+ )
+ end
+
it 'should have correct value in the compare dropdown' do
page.within '.mr-version-compare-dropdown' do
expect(page).to have_content 'version 1'
@@ -61,10 +72,6 @@ feature 'Merge Request versions', js: true, feature: true do
expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
end
- it 'show diff between new and old version' do
- expect(page).to have_content '4 changed files with 15 additions and 6 deletions'
- end
-
it 'should return to latest version when "Show latest version" button is clicked' do
click_link 'Show latest version'
page.within '.mr-version-dropdown' do
diff --git a/spec/features/merge_requests/user_uses_slash_commands_spec.rb b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
index 22d9d1b9fd5..cb3cea3fd51 100644
--- a/spec/features/merge_requests/user_uses_slash_commands_spec.rb
+++ b/spec/features/merge_requests/user_uses_slash_commands_spec.rb
@@ -14,21 +14,66 @@ feature 'Merge Requests > User uses slash commands', feature: true, js: true do
let(:new_url_opts) { { merge_request: { source_branch: 'feature' } } }
end
- describe 'adding a due date from note' do
+ describe 'merge-request-only commands' do
before do
project.team << [user, :master]
login_with(user)
visit namespace_project_merge_request_path(project.namespace, project, merge_request)
end
-
+
after do
wait_for_ajax
end
- it 'does not recognize the command nor create a note' do
- write_note("/due 2016-08-28")
+ describe 'toggling the WIP prefix in the title from note' do
+ context 'when the current user can toggle the WIP prefix' do
+ it 'adds the WIP: prefix to the title' do
+ write_note("/wip")
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload.work_in_progress?).to eq true
+ end
+
+ it 'removes the WIP: prefix from the title' do
+ merge_request.title = merge_request.wip_title
+ merge_request.save
+ write_note("/wip")
+
+ expect(page).not_to have_content '/wip'
+ expect(page).to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+
+ context 'when the current user cannot toggle the WIP prefix' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ logout
+ login_with(guest)
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ it 'does not change the WIP prefix' do
+ write_note("/wip")
+
+ expect(page).not_to have_content '/wip'
+ expect(page).not_to have_content 'Your commands have been executed!'
+
+ expect(merge_request.reload.work_in_progress?).to eq false
+ end
+ end
+ end
+
+ describe 'adding a due date from note' do
+ it 'does not recognize the command nor create a note' do
+ write_note('/due 2016-08-28')
- expect(page).not_to have_content '/due 2016-08-28'
+ expect(page).not_to have_content '/due 2016-08-28'
+ end
end
end
end
diff --git a/spec/features/notes_on_merge_requests_spec.rb b/spec/features/notes_on_merge_requests_spec.rb
index f1c522155d3..5d7247e2a62 100644
--- a/spec/features/notes_on_merge_requests_spec.rb
+++ b/spec/features/notes_on_merge_requests_spec.rb
@@ -240,6 +240,18 @@ describe 'Comments', feature: true do
is_expected.to have_css('.notes_holder .note', count: 1)
is_expected.to have_button('Reply...')
end
+
+ it 'adds code to discussion' do
+ click_button 'Reply...'
+
+ page.within(first('.js-discussion-note-form')) do
+ fill_in 'note[note]', with: '```{{ test }}```'
+
+ click_button('Comment')
+ end
+
+ expect(page).to have_content('{{ test }}')
+ end
end
end
end
diff --git a/spec/features/profiles/keys_spec.rb b/spec/features/profiles/keys_spec.rb
index 3b20d38c520..eb1050d21c6 100644
--- a/spec/features/profiles/keys_spec.rb
+++ b/spec/features/profiles/keys_spec.rb
@@ -1,18 +1,57 @@
require 'rails_helper'
-describe 'Profile > SSH Keys', feature: true do
+feature 'Profile > SSH Keys', feature: true do
let(:user) { create(:user) }
before do
login_as(user)
- visit profile_keys_path
end
- describe 'User adds an SSH key' do
- it 'auto-populates the title', js: true do
+ describe 'User adds a key' do
+ before do
+ visit profile_keys_path
+ end
+
+ scenario 'auto-populates the title', js: true do
fill_in('Key', with: attributes_for(:key).fetch(:key))
expect(find_field('Title').value).to eq 'dummy@gitlab.com'
end
+
+ scenario 'saves the new key' do
+ attrs = attributes_for(:key)
+
+ fill_in('Key', with: attrs[:key])
+ fill_in('Title', with: attrs[:title])
+ click_button('Add key')
+
+ expect(page).to have_content("Title: #{attrs[:title]}")
+ expect(page).to have_content(attrs[:key])
+ end
+ end
+
+ scenario 'User sees their keys' do
+ key = create(:key, user: user)
+ visit profile_keys_path
+
+ expect(page).to have_content(key.title)
+ end
+
+ scenario 'User removes a key via the key index' do
+ create(:key, user: user)
+ visit profile_keys_path
+
+ click_link('Remove')
+
+ expect(page).to have_content('Your SSH keys (0)')
+ end
+
+ scenario 'User removes a key via its details page' do
+ key = create(:key, user: user)
+ visit profile_key_path(key)
+
+ click_link('Remove')
+
+ expect(page).to have_content('Your SSH keys (0)')
end
end
diff --git a/spec/features/projects/badges/coverage_spec.rb b/spec/features/projects/badges/coverage_spec.rb
index 5972e7f31c2..01a95bf49ac 100644
--- a/spec/features/projects/badges/coverage_spec.rb
+++ b/spec/features/projects/badges/coverage_spec.rb
@@ -59,7 +59,7 @@ feature 'test coverage badge' do
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
- pipeline.build_updated
+ pipeline.update_status
end
end
diff --git a/spec/features/projects/files/edit_file_soft_wrap_spec.rb b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
new file mode 100644
index 00000000000..012befa7990
--- /dev/null
+++ b/spec/features/projects/files/edit_file_soft_wrap_spec.rb
@@ -0,0 +1,41 @@
+require 'spec_helper'
+
+feature 'User uses soft wrap whilst editing file', feature: true, js: true do
+ before do
+ user = create(:user)
+ project = create(:project)
+ project.team << [user, :master]
+ login_as user
+ visit namespace_project_new_blob_path(project.namespace, project, 'master', file_name: 'test_file-name')
+ editor = find('.file-editor.code')
+ editor.click
+ editor.send_keys 'Touch water with paw then recoil in horror chase dog then
+ run away chase the pig around the house eat owner\'s food, and knock
+ dish off table head butt cant eat out of my own dish. Cat is love, cat
+ is life rub face on everything poop on grasses so meow. Playing with
+ balls of wool flee in terror at cucumber discovered on floor run in
+ circles tuxedo cats always looking dapper, but attack dog, run away
+ and pretend to be victim so all of a sudden cat goes crazy, yet chase
+ laser. Make muffins sit in window and stare ooo, a bird! yum lick yarn
+ hanging out of own butt jump off balcony, onto stranger\'s head yet
+ chase laser. Purr for no reason stare at ceiling hola te quiero.'.squish
+ end
+
+ let(:toggle_button) { find('.soft-wrap-toggle') }
+
+ scenario 'user clicks the "Soft wrap" button and then "No wrap" button' do
+ wrapped_content_width = get_content_width
+ toggle_button.click
+ expect(toggle_button).to have_content 'No wrap'
+ unwrapped_content_width = get_content_width
+ expect(unwrapped_content_width).to be < wrapped_content_width
+
+ toggle_button.click
+ expect(toggle_button).to have_content 'Soft wrap'
+ expect(get_content_width).to be > unwrapped_content_width
+ end
+
+ def get_content_width
+ find('.ace_content')[:style].slice!(/width: \d+/).slice!(/\d+/)
+ end
+end
diff --git a/spec/features/projects/import_export/export_file_spec.rb b/spec/features/projects/import_export/export_file_spec.rb
index 27c986c5187..52d08982c7a 100644
--- a/spec/features/projects/import_export/export_file_spec.rb
+++ b/spec/features/projects/import_export/export_file_spec.rb
@@ -47,6 +47,8 @@ feature 'Import/Export - project export integration test', feature: true, js: tr
expect(page).to have_content('Download export')
+ expect(file_permissions(project.export_path)).to eq(0700)
+
in_directory_with_expanded_export(project) do |exit_status, tmpdir|
expect(exit_status).to eq(0)
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index 09cd6369881..f32834801a0 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -86,14 +86,14 @@ feature 'Import/Export - project import integration test', feature: true, js: tr
login_as(normal_user)
end
- scenario 'non-admin user is not allowed to import a project' do
+ scenario 'non-admin user is allowed to import a project' do
expect(Project.all.count).to be_zero
visit new_project_path
fill_in :project_path, with: 'test-project-path', visible: true
- expect(page).not_to have_content('GitLab export')
+ expect(page).to have_content('GitLab export')
end
end
diff --git a/spec/features/projects/issuable_templates_spec.rb b/spec/features/projects/issuable_templates_spec.rb
index f76c4fe8b57..cd79c4f512d 100644
--- a/spec/features/projects/issuable_templates_spec.rb
+++ b/spec/features/projects/issuable_templates_spec.rb
@@ -26,7 +26,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "bug" template' do
select_template 'bug'
wait_for_ajax
- preview_template
+ preview_template(template_content)
save_changes
end
@@ -42,6 +42,26 @@ feature 'issuable templates', feature: true, js: true do
end
end
+ context 'user creates an issue using templates, with a prior description' do
+ let(:prior_description) { 'test issue description' }
+ let(:template_content) { 'this is a test "bug" template' }
+ let(:issue) { create(:issue, author: user, assignee: user, project: project) }
+
+ background do
+ project.repository.commit_file(user, '.gitlab/issue_templates/bug.md', template_content, 'added issue template', 'master', false)
+ visit edit_namespace_project_issue_path project.namespace, project, issue
+ fill_in :'issue[title]', with: 'test issue title'
+ fill_in :'issue[description]', with: prior_description
+ end
+
+ scenario 'user selects "bug" template' do
+ select_template 'bug'
+ wait_for_ajax
+ preview_template("#{prior_description}\n\n#{template_content}")
+ save_changes
+ end
+ end
+
context 'user creates a merge request using templates' do
let(:template_content) { 'this is a test "feature-proposal" template' }
let(:merge_request) { create(:merge_request, :with_diffs, source_project: project) }
@@ -55,7 +75,7 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects "feature-proposal" template' do
select_template 'feature-proposal'
wait_for_ajax
- preview_template
+ preview_template(template_content)
save_changes
end
end
@@ -82,16 +102,16 @@ feature 'issuable templates', feature: true, js: true do
scenario 'user selects template' do
select_template 'feature-proposal'
wait_for_ajax
- preview_template
+ preview_template(template_content)
save_changes
end
end
end
end
- def preview_template
+ def preview_template(expected_content)
click_link 'Preview'
- expect(page).to have_content template_content
+ expect(page).to have_content expected_content
end
def save_changes
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
index 67811b1048e..6e948b7a616 100644
--- a/spec/features/projects/members/owner_cannot_leave_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -1,12 +1,10 @@
require 'spec_helper'
feature 'Projects > Members > Owner cannot leave project', feature: true do
- let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
- project.team << [owner, :owner]
- login_as(owner)
+ login_as(project.owner)
visit namespace_project_path(project.namespace, project)
end
diff --git a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
index 0e54c4fdf20..4ca9272b9c1 100644
--- a/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
+++ b/spec/features/projects/members/owner_cannot_request_access_to_his_project_spec.rb
@@ -1,12 +1,10 @@
require 'spec_helper'
feature 'Projects > Members > Owner cannot request access to his project', feature: true do
- let(:owner) { create(:user) }
let(:project) { create(:project) }
background do
- project.team << [owner, :owner]
- login_as(owner)
+ login_as(project.owner)
visit namespace_project_path(project.namespace, project)
end
diff --git a/spec/features/projects/snippets_spec.rb b/spec/features/projects/snippets_spec.rb
new file mode 100644
index 00000000000..d37e8ed4699
--- /dev/null
+++ b/spec/features/projects/snippets_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'Project snippets', feature: true do
+ context 'when the project has snippets' do
+ let(:project) { create(:empty_project, :public) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit namespace_project_snippets_path(project.namespace, project)
+ end
+
+ it_behaves_like 'paginated snippets'
+ end
+end
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 2242cb6236a..c30d38b6508 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -82,7 +82,7 @@ feature 'Project', feature: true do
before do
login_with(user)
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_path(project.namespace, project)
end
@@ -101,8 +101,8 @@ feature 'Project', feature: true do
context 'on issues page', js: true do
before do
login_with(user)
- project.team.add_user(user, Gitlab::Access::MASTER)
- project2.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
+ project2.add_user(user, Gitlab::Access::MASTER)
visit namespace_project_issue_path(project.namespace, project, issue)
end
diff --git a/spec/features/runners_spec.rb b/spec/features/runners_spec.rb
index a5ed3595b0a..0e1cc9a0f73 100644
--- a/spec/features/runners_spec.rb
+++ b/spec/features/runners_spec.rb
@@ -60,7 +60,7 @@ describe "Runners" do
it "removes specific runner for project if this is last project for that runners" do
within ".activated-specific-runners" do
- click_on "Remove runner"
+ click_on "Remove Runner"
end
expect(Ci::Runner.exists?(id: @specific_runner)).to be_falsey
@@ -75,7 +75,7 @@ describe "Runners" do
end
it "enables shared runners" do
- click_on "Enable shared runners"
+ click_on "Enable shared Runners"
expect(@project.reload.shared_runners_enabled).to be_truthy
end
end
diff --git a/spec/features/snippets_spec.rb b/spec/features/snippets_spec.rb
new file mode 100644
index 00000000000..70b16bfc810
--- /dev/null
+++ b/spec/features/snippets_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe 'Snippets', feature: true do
+ context 'when the project has snippets' do
+ let(:project) { create(:empty_project, :public) }
+ let!(:snippets) { create_list(:project_snippet, 2, :public, author: project.owner, project: project) }
+ before do
+ allow(Snippet).to receive(:default_per_page).and_return(1)
+ visit snippets_path(username: project.owner.username)
+ end
+
+ it_behaves_like 'paginated snippets'
+ end
+end
diff --git a/spec/features/todos/todos_spec.rb b/spec/features/todos/todos_spec.rb
index fc555a74f30..bf93c1d1251 100644
--- a/spec/features/todos/todos_spec.rb
+++ b/spec/features/todos/todos_spec.rb
@@ -4,7 +4,7 @@ describe 'Dashboard Todos', feature: true do
let(:user) { create(:user) }
let(:author) { create(:user) }
let(:project) { create(:project, visibility_level: Gitlab::VisibilityLevel::PUBLIC) }
- let(:issue) { create(:issue) }
+ let(:issue) { create(:issue, due_date: Date.today) }
describe 'GET /dashboard/todos' do
context 'User does not have todos' do
@@ -28,6 +28,12 @@ describe 'Dashboard Todos', feature: true do
expect(page).to have_selector('.todos-list .todo', count: 1)
end
+ it 'shows due date as today' do
+ page.within first('.todo') do
+ expect(page).to have_content 'Due today'
+ end
+ end
+
describe 'deleting the todo' do
before do
first('.done-todo').click
diff --git a/spec/features/unsubscribe_links_spec.rb b/spec/features/unsubscribe_links_spec.rb
index cc40671787c..33b52d1547e 100644
--- a/spec/features/unsubscribe_links_spec.rb
+++ b/spec/features/unsubscribe_links_spec.rb
@@ -11,7 +11,7 @@ describe 'Unsubscribe links', feature: true do
let(:mail) { ActionMailer::Base.deliveries.last }
let(:body) { Capybara::Node::Simple.new(mail.default_part_body.to_s) }
- let(:header_link) { mail.header['List-Unsubscribe'] }
+ let(:header_link) { mail.header['List-Unsubscribe'].to_s[1..-2] } # Strip angle brackets
let(:body_link) { body.find_link('unsubscribe')['href'] }
before do
diff --git a/spec/features/users/snippets_spec.rb b/spec/features/users/snippets_spec.rb
index f00abd82fea..ce7e809ec76 100644
--- a/spec/features/users/snippets_spec.rb
+++ b/spec/features/users/snippets_spec.rb
@@ -3,30 +3,16 @@ require 'spec_helper'
describe 'Snippets tab on a user profile', feature: true, js: true do
include WaitForAjax
- let(:user) { create(:user) }
-
context 'when the user has snippets' do
+ let(:user) { create(:user) }
+ let!(:snippets) { create_list(:snippet, 2, :public, author: user) }
before do
- create_list(:snippet, 25, :public, author: user)
-
+ allow(Snippet).to receive(:default_per_page).and_return(1)
visit user_path(user)
page.within('.user-profile-nav') { click_link 'Snippets' }
wait_for_ajax
end
- it 'is limited to 20 items per page' do
- expect(page.all('.snippets-list-holder .snippet-row').count).to eq(20)
- end
-
- context 'clicking on the link to the second page' do
- before do
- click_link('2')
- wait_for_ajax
- end
-
- it 'shows the remaining snippets' do
- expect(page.all('.snippets-list-holder .snippet-row').count).to eq(5)
- end
- end
+ it_behaves_like 'paginated snippets', remote: true
end
end
diff --git a/spec/features/users_spec.rb b/spec/features/users_spec.rb
index b5a94fe0383..6498b7317b4 100644
--- a/spec/features/users_spec.rb
+++ b/spec/features/users_spec.rb
@@ -40,6 +40,17 @@ feature 'Users', feature: true do
expect(number_of_errors_on_page(page)).to be(1), 'errors on page:\n #{errors_on_page page}'
end
+ describe 'redirect alias routes' do
+ before { user }
+
+ scenario '/u/user1 redirects to user page' do
+ visit '/u/user1'
+
+ expect(current_path).to eq user_path(user)
+ expect(page).to have_text(user.name)
+ end
+ end
+
def errors_on_page(page)
page.find('#error_explanation').find('ul').all('li').map{ |item| item.text }.join("\n")
end
diff --git a/spec/finders/access_requests_finder_spec.rb b/spec/finders/access_requests_finder_spec.rb
new file mode 100644
index 00000000000..8cfea9659cb
--- /dev/null
+++ b/spec/finders/access_requests_finder_spec.rb
@@ -0,0 +1,89 @@
+require 'spec_helper'
+
+describe AccessRequestsFinder, services: true do
+ let(:user) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
+
+ before do
+ project.request_access(access_requester)
+ group.request_access(access_requester)
+ end
+
+ shared_examples 'a finder returning access requesters' do |method_name|
+ it 'returns access requesters' do
+ access_requesters = described_class.new(source).public_send(method_name, user)
+
+ expect(access_requesters.size).to eq(1)
+ expect(access_requesters.first).to be_a "#{source.class}Member".constantize
+ expect(access_requesters.first.user).to eq(access_requester)
+ end
+ end
+
+ shared_examples 'a finder returning no results' do |method_name|
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect(described_class.new(source).public_send(method_name, user)).to be_empty
+ end
+ end
+
+ shared_examples 'a finder raising Gitlab::Access::AccessDeniedError' do |method_name|
+ it 'raises Gitlab::Access::AccessDeniedError' do
+ expect { described_class.new(source).public_send(method_name, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ describe '#execute' do
+ context 'when current user cannot see project access requests' do
+ it_behaves_like 'a finder returning no results', :execute do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder returning no results', :execute do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can see access requests' do
+ before do
+ project.team << [user, :master]
+ group.add_owner(user)
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute do
+ let(:source) { group }
+ end
+ end
+ end
+
+ describe '#execute!' do
+ context 'when current user cannot see access requests' do
+ it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder raising Gitlab::Access::AccessDeniedError', :execute! do
+ let(:source) { group }
+ end
+ end
+
+ context 'when current user can see access requests' do
+ before do
+ project.team << [user, :master]
+ group.add_owner(user)
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute! do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a finder returning access requesters', :execute! do
+ let(:source) { group }
+ end
+ end
+ end
+end
diff --git a/spec/finders/joined_groups_finder_spec.rb b/spec/finders/joined_groups_finder_spec.rb
index f90a8e007c8..29a47e005a6 100644
--- a/spec/finders/joined_groups_finder_spec.rb
+++ b/spec/finders/joined_groups_finder_spec.rb
@@ -43,7 +43,7 @@ describe JoinedGroupsFinder do
context 'if profile visitor is in one of the private group projects' do
before do
project = create(:project, :private, group: private_group, name: 'B', path: 'B')
- project.team.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
+ project.add_user(profile_visitor, Gitlab::Access::DEVELOPER)
end
it 'shows group' do
diff --git a/spec/finders/projects_finder_spec.rb b/spec/finders/projects_finder_spec.rb
index 7a3a74335e8..13bda5f7c5a 100644
--- a/spec/finders/projects_finder_spec.rb
+++ b/spec/finders/projects_finder_spec.rb
@@ -38,7 +38,7 @@ describe ProjectsFinder do
describe 'with private projects' do
before do
- private_project.team.add_user(user, Gitlab::Access::MASTER)
+ private_project.add_user(user, Gitlab::Access::MASTER)
end
it do
diff --git a/spec/finders/trending_projects_finder_spec.rb b/spec/finders/trending_projects_finder_spec.rb
deleted file mode 100644
index a49cbfd5160..00000000000
--- a/spec/finders/trending_projects_finder_spec.rb
+++ /dev/null
@@ -1,39 +0,0 @@
-require 'spec_helper'
-
-describe TrendingProjectsFinder do
- let(:user) { build(:user) }
-
- describe '#execute' do
- describe 'without an explicit start date' do
- subject { described_class.new }
-
- it 'returns the trending projects' do
- relation = double(:ar_relation)
-
- allow(subject).to receive(:projects_for)
- .with(user)
- .and_return(relation)
-
- allow(relation).to receive(:trending)
- .with(an_instance_of(ActiveSupport::TimeWithZone))
- end
- end
-
- describe 'with an explicit start date' do
- let(:date) { 2.months.ago }
-
- subject { described_class.new }
-
- it 'returns the trending projects' do
- relation = double(:ar_relation)
-
- allow(subject).to receive(:projects_for)
- .with(user)
- .and_return(relation)
-
- allow(relation).to receive(:trending)
- .with(date)
- end
- end
- end
-end
diff --git a/spec/helpers/broadcast_messages_helper_spec.rb b/spec/helpers/broadcast_messages_helper_spec.rb
index 157cc4665a2..c6e3c5c2368 100644
--- a/spec/helpers/broadcast_messages_helper_spec.rb
+++ b/spec/helpers/broadcast_messages_helper_spec.rb
@@ -7,7 +7,7 @@ describe BroadcastMessagesHelper do
end
it 'includes the current message' do
- current = double(message: 'Current Message')
+ current = BroadcastMessage.new(message: 'Current Message')
allow(helper).to receive(:broadcast_message_style).and_return(nil)
@@ -15,7 +15,7 @@ describe BroadcastMessagesHelper do
end
it 'includes custom style' do
- current = double(message: 'Current Message')
+ current = BroadcastMessage.new(message: 'Current Message')
allow(helper).to receive(:broadcast_message_style).and_return('foo')
diff --git a/spec/helpers/issuables_helper_spec.rb b/spec/helpers/issuables_helper_spec.rb
index 2dd2eab0524..62cc10f579a 100644
--- a/spec/helpers/issuables_helper_spec.rb
+++ b/spec/helpers/issuables_helper_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
-describe IssuablesHelper do
+describe IssuablesHelper do
let(:label) { build_stubbed(:label) }
let(:label2) { build_stubbed(:label) }
- context 'label tooltip' do
+ describe '#issuable_labels_tooltip' do
it 'returns label text' do
expect(issuable_labels_tooltip([label])).to eq(label.title)
end
@@ -13,4 +13,105 @@ describe IssuablesHelper do
expect(issuable_labels_tooltip([label, label2], limit: 1)).to eq("#{label.title}, and 1 more")
end
end
+
+ describe '#issuables_state_counter_text' do
+ let(:user) { create(:user) }
+
+ describe 'state text' do
+ before do
+ allow(helper).to receive(:issuables_count_for_state).and_return(42)
+ end
+
+ it 'returns "Open" when state is :opened' do
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+
+ it 'returns "Closed" when state is :closed' do
+ expect(helper.issuables_state_counter_text(:issues, :closed)).
+ to eq('<span>Closed</span> <span class="badge">42</span>')
+ end
+
+ it 'returns "Merged" when state is :merged' do
+ expect(helper.issuables_state_counter_text(:merge_requests, :merged)).
+ to eq('<span>Merged</span> <span class="badge">42</span>')
+ end
+
+ it 'returns "All" when state is :all' do
+ expect(helper.issuables_state_counter_text(:merge_requests, :all)).
+ to eq('<span>All</span> <span class="badge">42</span>')
+ end
+ end
+
+ describe 'counter caching based on issuable type and params', :caching do
+ let(:params) do
+ {
+ scope: 'created-by-me',
+ state: 'opened',
+ utf8: '✓',
+ author_id: '11',
+ assignee_id: '18',
+ label_name: ['bug', 'discussion', 'documentation'],
+ milestone_title: 'v4.0',
+ sort: 'due_date_asc',
+ namespace_id: 'gitlab-org',
+ project_id: 'gitlab-ce',
+ page: 2
+ }.with_indifferent_access
+ end
+
+ it 'returns the cached value when called for the same issuable type & with the same params' do
+ expect(helper).to receive(:params).twice.and_return(params)
+ expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+
+ expect(helper).not_to receive(:issuables_count_for_state)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+
+ it 'does not take some keys into account in the cache key' do
+ expect(helper).to receive(:params).and_return({
+ author_id: '11',
+ state: 'foo',
+ sort: 'foo',
+ utf8: 'foo',
+ page: 'foo'
+ }.with_indifferent_access)
+ expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+
+ expect(helper).to receive(:params).and_return({
+ author_id: '11',
+ state: 'bar',
+ sort: 'bar',
+ utf8: 'bar',
+ page: 'bar'
+ }.with_indifferent_access)
+ expect(helper).not_to receive(:issuables_count_for_state)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+
+ it 'does not take params order into account in the cache key' do
+ expect(helper).to receive(:params).and_return('author_id' => '11', 'state' => 'opened')
+ expect(helper).to receive(:issuables_count_for_state).with(:issues, :opened).and_return(42)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+
+ expect(helper).to receive(:params).and_return('state' => 'opened', 'author_id' => '11')
+ expect(helper).not_to receive(:issuables_count_for_state)
+
+ expect(helper.issuables_state_counter_text(:issues, :opened)).
+ to eq('<span>Open</span> <span class="badge">42</span>')
+ end
+ end
+ end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 67bac782591..abe08d95ece 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -63,28 +63,38 @@ describe IssuesHelper do
end
describe '#award_user_list' do
- let!(:awards) { build_list(:award_emoji, 15) }
+ it "returns a comma-separated list of the first X users" do
+ user = build_stubbed(:user, name: 'Joe')
+ awards = Array.new(3, build_stubbed(:award_emoji, user: user))
- it "returns a comma seperated list of 1-9 users" do
- expect(award_user_list(awards.first(9), nil)).to eq(awards.first(9).map { |a| a.user.name }.to_sentence)
+ expect(award_user_list(awards, nil, limit: 3))
+ .to eq('Joe, Joe, and Joe')
end
it "displays the current user's name as 'You'" do
- expect(award_user_list(awards.first(1), awards[0].user)).to eq('You')
- end
+ user = build_stubbed(:user, name: 'Joe')
+ award = build_stubbed(:award_emoji, user: user)
- it "truncates lists of larger than 9 users" do
- expect(award_user_list(awards, nil)).to eq(awards.first(9).map { |a| a.user.name }.join(', ') + ", and 6 more.")
+ expect(award_user_list([award], user)).to eq('You')
+ expect(award_user_list([award], nil)).to eq 'Joe'
end
- it "displays the current user in front of 0-9 other users" do
- expect(award_user_list(awards, awards[0].user)).
- to eq("You, " + awards[1..9].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ it "truncates lists" do
+ user = build_stubbed(:user, name: 'Jane')
+ awards = Array.new(5, build_stubbed(:award_emoji, user: user))
+
+ expect(award_user_list(awards, nil, limit: 3))
+ .to eq('Jane, Jane, Jane, and 2 more.')
end
- it "displays the current user in front regardless of position in the list" do
- expect(award_user_list(awards, awards[12].user)).
- to eq("You, " + awards[0..8].map { |a| a.user.name }.join(', ') + ", and 5 more.")
+ it "displays the current user in front of other users" do
+ current_user = build_stubbed(:user)
+ my_award = build_stubbed(:award_emoji, user: current_user)
+ award = build_stubbed(:award_emoji, user: build_stubbed(:user, name: 'Jane'))
+ awards = Array.new(5, award).push(my_award)
+
+ expect(award_user_list(awards, current_user, limit: 2)).
+ to eq("You, Jane, and 4 more.")
end
end
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 7998209b7b0..6703d88e357 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -11,7 +11,7 @@ describe MembersHelper do
describe '#remove_member_message' do
let(:requester) { build(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_invite) { build(:project_member, project: project).tap { |m| m.generate_invite_token! } }
let(:project_member_request) { project.request_access(requester) }
@@ -32,7 +32,7 @@ describe MembersHelper do
describe '#remove_member_title' do
let(:requester) { build(:user) }
- let(:project) { create(:project) }
+ let(:project) { create(:empty_project, :public) }
let(:project_member) { build(:project_member, project: project) }
let(:project_member_request) { project.request_access(requester) }
let(:group) { create(:group) }
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 70032e7df94..8113742923b 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -11,7 +11,7 @@ describe ProjectsHelper do
describe "can_change_visibility_level?" do
let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:user) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:fork_project) { Projects::ForkService.new(project, user).execute }
it "returns false if there are no appropriate permissions" do
@@ -72,7 +72,7 @@ describe ProjectsHelper do
it 'returns an HTML link to the user' do
link = helper.link_to_member(project, user)
- expect(link).to match(%r{/u/#{user.username}})
+ expect(link).to match(%r{/#{user.username}})
end
end
end
diff --git a/spec/javascripts/activities_spec.js.es6 b/spec/javascripts/activities_spec.js.es6
new file mode 100644
index 00000000000..743b15460c6
--- /dev/null
+++ b/spec/javascripts/activities_spec.js.es6
@@ -0,0 +1,61 @@
+/*= require jquery.cookie.js */
+/*= require jquery.endless-scroll.js */
+/*= require pager */
+/*= require activities */
+
+(() => {
+ window.gon || (window.gon = {});
+ const fixtureTemplate = 'event_filter.html';
+ const filters = [
+ {
+ id: 'all',
+ }, {
+ id: 'push',
+ name: 'push events',
+ }, {
+ id: 'merged',
+ name: 'merge events',
+ }, {
+ id: 'comments',
+ },{
+ id: 'team',
+ }];
+
+ function getEventName(index) {
+ let filter = filters[index];
+ return filter.hasOwnProperty('name') ? filter.name : filter.id;
+ }
+
+ function getSelector(index) {
+ let filter = filters[index];
+ return `#${filter.id}_event_filter`
+ }
+
+ describe('Activities', () => {
+ beforeEach(() => {
+ fixture.load(fixtureTemplate);
+ new Activities();
+ });
+
+ for(let i = 0; i < filters.length; i++) {
+ ((i) => {
+ describe(`when selecting ${getEventName(i)}`, () => {
+ beforeEach(() => {
+ $(getSelector(i)).click();
+ });
+
+ for(let x = 0; x < filters.length; x++) {
+ ((x) => {
+ let shouldHighlight = i === x;
+ let testName = shouldHighlight ? 'should highlight' : 'should not highlight';
+
+ it(`${testName} ${getEventName(x)}`, () => {
+ expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight);
+ });
+ })(x);
+ }
+ });
+ })(i);
+ }
+ });
+})();
diff --git a/spec/javascripts/fixtures/event_filter.html.haml b/spec/javascripts/fixtures/event_filter.html.haml
new file mode 100644
index 00000000000..95e248cadf8
--- /dev/null
+++ b/spec/javascripts/fixtures/event_filter.html.haml
@@ -0,0 +1,21 @@
+%ul.nav-links.event-filter.scrolling-tabs
+ %li.active
+ %a.event-filter-link{ id: "all_event_filter", title: "Filter by all", href: "/dashboard/activity"}
+ %span
+ All
+ %li
+ %a.event-filter-link{ id: "push_event_filter", title: "Filter by push events", href: "/dashboard/activity"}
+ %span
+ Push events
+ %li
+ %a.event-filter-link{ id: "merged_event_filter", title: "Filter by merge events", href: "/dashboard/activity"}
+ %span
+ Merge events
+ %li
+ %a.event-filter-link{ id: "comments_event_filter", title: "Filter by comments", href: "/dashboard/activity"}
+ %span
+ Comments
+ %li
+ %a.event-filter-link{ id: "team_event_filter", title: "Filter by team", href: "/dashboard/activity"}
+ %span
+ Team \ No newline at end of file
diff --git a/spec/javascripts/labels_issue_sidebar_spec.js.es6 b/spec/javascripts/labels_issue_sidebar_spec.js.es6
index 840c7b6d015..1ad6f612210 100644
--- a/spec/javascripts/labels_issue_sidebar_spec.js.es6
+++ b/spec/javascripts/labels_issue_sidebar_spec.js.es6
@@ -48,9 +48,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
- $('.dropdow-content a').each((i, $link) => {
- if (i < 5) {
- $link.get(0).click();
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
}
});
@@ -70,9 +70,9 @@
setTimeout(() => {
expect($('.dropdown-content a').length).toBe(10);
- $('.dropdow-content a').each((i, $link) => {
- if (i < 5) {
- $link.get(0).click();
+ $('.dropdown-content a').each(function (i) {
+ if (i < saveLabelCount) {
+ $(this).get(0).click();
}
});
@@ -86,4 +86,3 @@
});
});
})();
-
diff --git a/spec/javascripts/search_autocomplete_spec.js b/spec/javascripts/search_autocomplete_spec.js
index 00d9fc1302a..333128782a2 100644
--- a/spec/javascripts/search_autocomplete_spec.js
+++ b/spec/javascripts/search_autocomplete_spec.js
@@ -5,6 +5,8 @@
/*= require lib/utils/common_utils */
/*= require lib/utils/type_utility */
/*= require fuzzaldrin-plus */
+/*= require turbolinks */
+/*= require jquery.turbolinks */
(function() {
var addBodyAttributes, assertLinks, dashboardIssuesPath, dashboardMRsPath, groupIssuesPath, groupMRsPath, groupName, mockDashboardOptions, mockGroupOptions, mockProjectOptions, projectIssuesPath, projectMRsPath, projectName, userId, widget;
@@ -112,7 +114,7 @@
fixture.preload('search_autocomplete.html');
beforeEach(function() {
fixture.load('search_autocomplete.html');
- return widget = new SearchAutocomplete;
+ return widget = new gl.SearchAutocomplete;
});
it('should show Dashboard specific dropdown menu', function() {
var list;
@@ -138,7 +140,7 @@
list = widget.wrap.find('.dropdown-menu').find('ul');
return assertLinks(list, projectIssuesPath, projectMRsPath);
});
- return it('should not show category related menu if there is text in the input', function() {
+ it('should not show category related menu if there is text in the input', function() {
var link, list;
addBodyAttributes('project');
mockProjectOptions();
@@ -148,6 +150,23 @@
link = "a[href='" + projectIssuesPath + "/?assignee_id=" + userId + "']";
return expect(list.find(link).length).toBe(0);
});
+ return it('should not submit the search form when selecting an autocomplete row with the keyboard', function() {
+ var ENTER = 13;
+ var DOWN = 40;
+ addBodyAttributes();
+ mockDashboardOptions(true);
+ var submitSpy = spyOnEvent('form', 'submit');
+ widget.searchInput.focus();
+ widget.wrap.trigger($.Event('keydown', { which: DOWN }));
+ var enterKeyEvent = $.Event('keydown', { which: ENTER });
+ widget.searchInput.trigger(enterKeyEvent);
+ // This does not currently catch failing behavior. For security reasons,
+ // browsers will not trigger default behavior (form submit, in this
+ // example) on JavaScript-created keypresses.
+ expect(submitSpy).not.toHaveBeenTriggered();
+ // Does a worse job at capturing the intent of the test, but works.
+ expect(enterKeyEvent.isDefaultPrevented()).toBe(true);
+ });
});
}).call(this);
diff --git a/spec/lib/banzai/filter/html_entity_filter_spec.rb b/spec/lib/banzai/filter/html_entity_filter_spec.rb
new file mode 100644
index 00000000000..4c68ce6d6e4
--- /dev/null
+++ b/spec/lib/banzai/filter/html_entity_filter_spec.rb
@@ -0,0 +1,14 @@
+require 'spec_helper'
+
+describe Banzai::Filter::HtmlEntityFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:unescaped) { 'foo <strike attr="foo">&&&</strike>' }
+ let(:escaped) { 'foo &lt;strike attr=&quot;foo&quot;&gt;&amp;&amp;&amp;&lt;/strike&gt;' }
+
+ it 'converts common entities to their HTML-escaped equivalents' do
+ output = filter(unescaped)
+
+ expect(output).to eq(escaped)
+ end
+end
diff --git a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
index b1370bca833..d265d29ee86 100644
--- a/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
+++ b/spec/lib/banzai/filter/syntax_highlight_filter_spec.rb
@@ -6,21 +6,21 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
context "when no language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code>def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>def fun end</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>def fun end</code></pre>')
end
end
context "when a valid language is specified" do
it "highlights as that language" do
result = filter('<pre><code class="ruby">def fun end</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight ruby" v-pre="true"><code><span class="k">def</span> <span class="nf">fun</span> <span class="k">end</span></code></pre>')
end
end
context "when an invalid language is specified" do
it "highlights as plaintext" do
result = filter('<pre><code class="gnuplot">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight js-syntax-highlight plaintext" v-pre="true"><code>This is a test</code></pre>')
end
end
@@ -31,7 +31,7 @@ describe Banzai::Filter::SyntaxHighlightFilter, lib: true do
it "highlights as plaintext" do
result = filter('<pre><code class="ruby">This is a test</code></pre>')
- expect(result.to_html).to eq('<pre class="code highlight"><code>This is a test</code></pre>')
+ expect(result.to_html).to eq('<pre class="code highlight" v-pre="true"><code>This is a test</code></pre>')
end
end
end
diff --git a/spec/lib/banzai/filter/task_list_filter_spec.rb b/spec/lib/banzai/filter/task_list_filter_spec.rb
deleted file mode 100644
index 569cbc885c7..00000000000
--- a/spec/lib/banzai/filter/task_list_filter_spec.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-require 'spec_helper'
-
-describe Banzai::Filter::TaskListFilter, lib: true do
- include FilterSpecHelper
-
- it 'does not apply `task-list` class to non-task lists' do
- exp = act = %(<ul><li>Item</li></ul>)
- expect(filter(act).to_html).to eq exp
- end
-
- it 'applies `task-list` to single-item task lists' do
- act = filter('<ul><li>[ ] Task 1</li></ul>')
-
- expect(act.to_html).to start_with '<ul class="task-list">'
- end
-end
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index fdbdb21eac1..729e77fd43f 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -31,13 +31,16 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
end
it 'supports a special @all mention' do
+ project.team << [user, :developer]
doc = reference_filter("Hey #{reference}", author: user)
+
expect(doc.css('a').length).to eq 1
expect(doc.css('a').first.attr('href'))
.to eq urls.namespace_project_url(project.namespace, project)
end
it 'includes a data-author attribute when there is an author' do
+ project.team << [user, :developer]
doc = reference_filter(reference, author: user)
expect(doc.css('a').first.attr('data-author')).to eq(user.id.to_s)
@@ -48,6 +51,12 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(doc.css('a').first.has_attribute?('data-author')).to eq(false)
end
+
+ it 'ignores reference to all when the user is not a project member' do
+ doc = reference_filter("Hey #{reference}", author: user)
+
+ expect(doc.css('a').length).to eq 0
+ end
end
context 'mentioning a user' do
diff --git a/spec/lib/banzai/note_renderer_spec.rb b/spec/lib/banzai/note_renderer_spec.rb
index 98f76f36fd5..49556074278 100644
--- a/spec/lib/banzai/note_renderer_spec.rb
+++ b/spec/lib/banzai/note_renderer_spec.rb
@@ -12,8 +12,7 @@ describe Banzai::NoteRenderer do
with(project, user,
requested_path: 'foo',
project_wiki: wiki,
- ref: 'bar',
- pipeline: :note).
+ ref: 'bar').
and_call_original
expect_any_instance_of(Banzai::ObjectRenderer).
diff --git a/spec/lib/banzai/object_renderer_spec.rb b/spec/lib/banzai/object_renderer_spec.rb
index bcdb95250ca..90da78a67dd 100644
--- a/spec/lib/banzai/object_renderer_spec.rb
+++ b/spec/lib/banzai/object_renderer_spec.rb
@@ -4,10 +4,18 @@ describe Banzai::ObjectRenderer do
let(:project) { create(:empty_project) }
let(:user) { project.owner }
+ def fake_object(attrs = {})
+ object = double(attrs.merge("new_record?" => true, "destroyed?" => true))
+ allow(object).to receive(:markdown_cache_field_for).with(:note).and_return(:note_html)
+ allow(object).to receive(:banzai_render_context).with(:note).and_return(project: nil, author: nil)
+ allow(object).to receive(:update_column).with(:note_html, anything).and_return(true)
+ object
+ end
+
describe '#render' do
it 'renders and redacts an Array of objects' do
renderer = described_class.new(project, user)
- object = double(:object, note: 'hello', note_html: nil)
+ object = fake_object(note: 'hello', note_html: nil)
expect(renderer).to receive(:render_objects).with([object], :note).
and_call_original
@@ -16,7 +24,7 @@ describe Banzai::ObjectRenderer do
with(an_instance_of(Array)).
and_call_original
- expect(object).to receive(:note_html=).with('<p>hello</p>')
+ expect(object).to receive(:redacted_note_html=).with('<p>hello</p>')
expect(object).to receive(:user_visible_reference_count=).with(0)
renderer.render([object], :note)
@@ -25,7 +33,7 @@ describe Banzai::ObjectRenderer do
describe '#render_objects' do
it 'renders an Array of objects' do
- object = double(:object, note: 'hello')
+ object = fake_object(note: 'hello', note_html: nil)
renderer = described_class.new(project, user)
@@ -57,49 +65,29 @@ describe Banzai::ObjectRenderer do
end
describe '#context_for' do
- let(:object) { double(:object, note: 'hello') }
+ let(:object) { fake_object(note: 'hello') }
let(:renderer) { described_class.new(project, user) }
it 'returns a Hash' do
expect(renderer.context_for(object, :note)).to be_an_instance_of(Hash)
end
- it 'includes the cache key' do
+ it 'includes the banzai render context for the object' do
+ expect(object).to receive(:banzai_render_context).with(:note).and_return(foo: :bar)
context = renderer.context_for(object, :note)
-
- expect(context[:cache_key]).to eq([object, :note])
- end
-
- context 'when the object responds to "author"' do
- it 'includes the author in the context' do
- expect(object).to receive(:author).and_return('Alice')
-
- context = renderer.context_for(object, :note)
-
- expect(context[:author]).to eq('Alice')
- end
- end
-
- context 'when the object does not respond to "author"' do
- it 'does not include the author in the context' do
- context = renderer.context_for(object, :note)
-
- expect(context.key?(:author)).to eq(false)
- end
+ expect(context).to have_key(:foo)
+ expect(context[:foo]).to eq(:bar)
end
end
describe '#render_attributes' do
it 'renders the attribute of a list of objects' do
- objects = [double(:doc, note: 'hello'), double(:doc, note: 'bye')]
- renderer = described_class.new(project, user, pipeline: :note)
+ objects = [fake_object(note: 'hello', note_html: nil), fake_object(note: 'bye', note_html: nil)]
+ renderer = described_class.new(project, user)
- expect(Banzai).to receive(:cache_collection_render).
- with([
- { text: 'hello', context: renderer.context_for(objects[0], :note) },
- { text: 'bye', context: renderer.context_for(objects[1], :note) }
- ]).
- and_call_original
+ objects.each do |object|
+ expect(Banzai).to receive(:render_field).with(object, :note).and_call_original
+ end
docs = renderer.render_attributes(objects, :note)
@@ -114,17 +102,13 @@ describe Banzai::ObjectRenderer do
objects = []
renderer = described_class.new(project, user, pipeline: :note)
- expect(Banzai).to receive(:cache_collection_render).
- with([]).
- and_call_original
-
expect(renderer.render_attributes(objects, :note)).to eq([])
end
end
describe '#base_context' do
let(:context) do
- described_class.new(project, user, pipeline: :note).base_context
+ described_class.new(project, user, foo: :bar).base_context
end
it 'returns a Hash' do
@@ -132,7 +116,7 @@ describe Banzai::ObjectRenderer do
end
it 'includes the custom attributes' do
- expect(context[:pipeline]).to eq(:note)
+ expect(context[:foo]).to eq(:bar)
end
it 'includes the current user' do
diff --git a/spec/lib/banzai/renderer_spec.rb b/spec/lib/banzai/renderer_spec.rb
new file mode 100644
index 00000000000..aaa6b12e67e
--- /dev/null
+++ b/spec/lib/banzai/renderer_spec.rb
@@ -0,0 +1,74 @@
+require 'spec_helper'
+
+describe Banzai::Renderer do
+ def expect_render(project = :project)
+ expected_context = { project: project }
+ expect(renderer).to receive(:cacheless_render) { :html }.with(:markdown, expected_context)
+ end
+
+ def expect_cache_update
+ expect(object).to receive(:update_column).with("field_html", :html)
+ end
+
+ def fake_object(*features)
+ markdown = :markdown if features.include?(:markdown)
+ html = :html if features.include?(:html)
+
+ object = double(
+ "object",
+ banzai_render_context: { project: :project },
+ field: markdown,
+ field_html: html
+ )
+
+ allow(object).to receive(:markdown_cache_field_for).with(:field).and_return("field_html")
+ allow(object).to receive(:new_record?).and_return(features.include?(:new))
+ allow(object).to receive(:destroyed?).and_return(features.include?(:destroyed))
+
+ object
+ end
+
+ describe "#render_field" do
+ let(:renderer) { Banzai::Renderer }
+ let(:subject) { renderer.render_field(object, :field) }
+
+ context "with an empty cache" do
+ let(:object) { fake_object(:markdown) }
+ it "caches and returns the result" do
+ expect_render
+ expect_cache_update
+ expect(subject).to eq(:html)
+ end
+ end
+
+ context "with a filled cache" do
+ let(:object) { fake_object(:markdown, :html) }
+
+ it "uses the cache" do
+ expect_render.never
+ expect_cache_update.never
+ should eq(:html)
+ end
+ end
+
+ context "new object" do
+ let(:object) { fake_object(:new, :markdown) }
+
+ it "doesn't cache the result" do
+ expect_render
+ expect_cache_update.never
+ expect(subject).to eq(:html)
+ end
+ end
+
+ context "destroyed object" do
+ let(:object) { fake_object(:destroyed, :markdown) }
+
+ it "doesn't cache the result" do
+ expect_render
+ expect_cache_update.never
+ expect(subject).to eq(:html)
+ end
+ end
+ end
+end
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
new file mode 100644
index 00000000000..f0b75a664f2
--- /dev/null
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe GroupUrlConstrainer, lib: true do
+ let!(:username) { create(:group, path: 'gitlab-org') }
+
+ describe '#find_resource' do
+ it { expect(!!subject.find_resource('gitlab-org')).to be_truthy }
+ it { expect(!!subject.find_resource('gitlab-com')).to be_falsey }
+ end
+end
diff --git a/spec/lib/constraints/namespace_url_constrainer_spec.rb b/spec/lib/constraints/namespace_url_constrainer_spec.rb
new file mode 100644
index 00000000000..a5feaacb8ee
--- /dev/null
+++ b/spec/lib/constraints/namespace_url_constrainer_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe NamespaceUrlConstrainer, lib: true do
+ let!(:group) { create(:group, path: 'gitlab') }
+
+ describe '#matches?' do
+ context 'existing namespace' do
+ it { expect(subject.matches?(request '/gitlab')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab.atom')).to be_truthy }
+ it { expect(subject.matches?(request '/gitlab/')).to be_truthy }
+ it { expect(subject.matches?(request '//gitlab/')).to be_truthy }
+ end
+
+ context 'non-existing namespace' do
+ it { expect(subject.matches?(request '/gitlab-ce')).to be_falsey }
+ it { expect(subject.matches?(request '/gitlab.ce')).to be_falsey }
+ it { expect(subject.matches?(request '/g/gitlab')).to be_falsey }
+ it { expect(subject.matches?(request '/.gitlab')).to be_falsey }
+ end
+ end
+
+ def request(path)
+ OpenStruct.new(path: path)
+ end
+end
diff --git a/spec/lib/constraints/user_url_constrainer_spec.rb b/spec/lib/constraints/user_url_constrainer_spec.rb
new file mode 100644
index 00000000000..4b26692672f
--- /dev/null
+++ b/spec/lib/constraints/user_url_constrainer_spec.rb
@@ -0,0 +1,10 @@
+require 'spec_helper'
+
+describe UserUrlConstrainer, lib: true do
+ let!(:username) { create(:user, username: 'dz') }
+
+ describe '#find_resource' do
+ it { expect(!!subject.find_resource('dz')).to be_truthy }
+ it { expect(!!subject.find_resource('john')).to be_falsey }
+ end
+end
diff --git a/spec/lib/event_filter_spec.rb b/spec/lib/event_filter_spec.rb
new file mode 100644
index 00000000000..a6d8e6927e0
--- /dev/null
+++ b/spec/lib/event_filter_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe EventFilter, lib: true do
+ describe '#apply_filter' do
+ let(:source_user) { create(:user) }
+ let!(:public_project) { create(:project, :public) }
+
+ let!(:push_event) { create(:event, action: Event::PUSHED, project: public_project, target: public_project, author: source_user) }
+ let!(:merged_event) { create(:event, action: Event::MERGED, project: public_project, target: public_project, author: source_user) }
+ let!(:comments_event) { create(:event, action: Event::COMMENTED, project: public_project, target: public_project, author: source_user) }
+ let!(:joined_event) { create(:event, action: Event::JOINED, project: public_project, target: public_project, author: source_user) }
+ let!(:left_event) { create(:event, action: Event::LEFT, project: public_project, target: public_project, author: source_user) }
+
+ it 'applies push filter' do
+ events = EventFilter.new(EventFilter.push).apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event)
+ end
+
+ it 'applies merged filter' do
+ events = EventFilter.new(EventFilter.merged).apply_filter(Event.all)
+ expect(events).to contain_exactly(merged_event)
+ end
+
+ it 'applies comments filter' do
+ events = EventFilter.new(EventFilter.comments).apply_filter(Event.all)
+ expect(events).to contain_exactly(comments_event)
+ end
+
+ it 'applies team filter' do
+ events = EventFilter.new(EventFilter.team).apply_filter(Event.all)
+ expect(events).to contain_exactly(joined_event, left_event)
+ end
+
+ it 'applies all filter' do
+ events = EventFilter.new(EventFilter.all).apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ end
+
+ it 'applies no filter' do
+ events = EventFilter.new(nil).apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ end
+
+ it 'applies unknown filter' do
+ events = EventFilter.new('').apply_filter(Event.all)
+ expect(events).to contain_exactly(push_event, merged_event, comments_event, joined_event, left_event)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/auth_spec.rb b/spec/lib/gitlab/auth_spec.rb
index 745fbc0df45..c9d64e99f88 100644
--- a/spec/lib/gitlab/auth_spec.rb
+++ b/spec/lib/gitlab/auth_spec.rb
@@ -64,7 +64,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes user lfs tokens' do
user = create(:user)
ip = 'ip'
- token = Gitlab::LfsToken.new(user).generate
+ token = Gitlab::LfsToken.new(user).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: user.username)
expect(gl_auth.find_for_git_client(user.username, token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(user, nil, :lfs_token, full_authentication_abilities))
@@ -73,7 +73,7 @@ describe Gitlab::Auth, lib: true do
it 'recognizes deploy key lfs tokens' do
key = create(:deploy_key)
ip = 'ip'
- token = Gitlab::LfsToken.new(key).generate
+ token = Gitlab::LfsToken.new(key).token
expect(gl_auth).to receive(:rate_limit!).with(ip, success: true, login: "lfs+deploy-key-#{key.id}")
expect(gl_auth.find_for_git_client("lfs+deploy-key-#{key.id}", token, project: nil, ip: ip)).to eq(Gitlab::Auth::Result.new(key, nil, :lfs_deploy_token, read_authentication_abilities))
diff --git a/spec/lib/gitlab/backend/shell_spec.rb b/spec/lib/gitlab/backend/shell_spec.rb
index 07407f212aa..f826d0d1b04 100644
--- a/spec/lib/gitlab/backend/shell_spec.rb
+++ b/spec/lib/gitlab/backend/shell_spec.rb
@@ -22,15 +22,15 @@ describe Gitlab::Shell, lib: true do
it { expect(gitlab_shell.url_to_repo('diaspora')).to eq(Gitlab.config.gitlab_shell.ssh_path_prefix + "diaspora.git") }
- describe 'generate_and_link_secret_token' do
+ describe 'memoized secret_token' do
let(:secret_file) { 'tmp/tests/.secret_shell_test' }
let(:link_file) { 'tmp/tests/shell-secret-test/.gitlab_shell_secret' }
before do
- allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
allow(Gitlab.config.gitlab_shell).to receive(:secret_file).and_return(secret_file)
+ allow(Gitlab.config.gitlab_shell).to receive(:path).and_return('tmp/tests/shell-secret-test')
FileUtils.mkdir('tmp/tests/shell-secret-test')
- gitlab_shell.generate_and_link_secret_token
+ Gitlab::Shell.ensure_secret_token!
end
after do
@@ -39,7 +39,10 @@ describe Gitlab::Shell, lib: true do
end
it 'creates and links the secret token file' do
+ secret_token = Gitlab::Shell.secret_token
+
expect(File.exist?(secret_file)).to be(true)
+ expect(File.read(secret_file).chomp).to eq(secret_token)
expect(File.symlink?(link_file)).to be(true)
expect(File.readlink(link_file)).to eq(secret_file)
end
diff --git a/spec/lib/gitlab/badge/coverage/report_spec.rb b/spec/lib/gitlab/badge/coverage/report_spec.rb
index ab0cce6e091..1547bd3228c 100644
--- a/spec/lib/gitlab/badge/coverage/report_spec.rb
+++ b/spec/lib/gitlab/badge/coverage/report_spec.rb
@@ -100,7 +100,7 @@ describe Gitlab::Badge::Coverage::Report do
create(:ci_pipeline, opts).tap do |pipeline|
yield pipeline
- pipeline.build_updated
+ pipeline.update_status
end
end
end
diff --git a/spec/lib/gitlab/github_import/client_spec.rb b/spec/lib/gitlab/github_import/client_spec.rb
index 613c47d55f1..e829b936343 100644
--- a/spec/lib/gitlab/github_import/client_spec.rb
+++ b/spec/lib/gitlab/github_import/client_spec.rb
@@ -66,6 +66,6 @@ describe Gitlab::GithubImport::Client, lib: true do
stub_request(:get, /api.github.com/)
allow(client.api).to receive(:rate_limit!).and_raise(Octokit::NotFound)
- expect { client.issues }.not_to raise_error
+ expect { client.issues {} }.not_to raise_error
end
end
diff --git a/spec/lib/gitlab/github_import/importer_spec.rb b/spec/lib/gitlab/github_import/importer_spec.rb
index 553c849c9b4..8854c8431b5 100644
--- a/spec/lib/gitlab/github_import/importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer_spec.rb
@@ -57,7 +57,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347'
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1347',
+ labels: [double(name: 'Label #1')],
)
end
@@ -75,7 +76,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
created_at: created_at,
updated_at: updated_at,
closed_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348'
+ url: 'https://api.github.com/repos/octocat/Hello-World/issues/1348',
+ labels: [double(name: 'Label #2')],
)
end
@@ -94,7 +96,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
updated_at: updated_at,
closed_at: nil,
merged_at: nil,
- url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347'
+ url: 'https://api.github.com/repos/octocat/Hello-World/pulls/1347',
+ labels: [double(name: 'Label #3')],
)
end
@@ -129,6 +132,8 @@ describe Gitlab::GithubImport::Importer, lib: true do
allow_any_instance_of(Octokit::Client).to receive(:milestones).and_return([milestone, milestone])
allow_any_instance_of(Octokit::Client).to receive(:issues).and_return([issue1, issue2])
allow_any_instance_of(Octokit::Client).to receive(:pull_requests).and_return([pull_request, pull_request])
+ allow_any_instance_of(Octokit::Client).to receive(:issues_comments).and_return([])
+ allow_any_instance_of(Octokit::Client).to receive(:pull_requests_comments).and_return([])
allow_any_instance_of(Octokit::Client).to receive(:last_response).and_return(double(rels: { next: nil }))
allow_any_instance_of(Octokit::Client).to receive(:releases).and_return([release1, release2])
allow_any_instance_of(Gitlab::Shell).to receive(:import_repository).and_raise(Gitlab::Shell::Error)
@@ -148,9 +153,7 @@ describe Gitlab::GithubImport::Importer, lib: true do
errors: [
{ type: :label, url: "https://api.github.com/repos/octocat/Hello-World/labels/bug", errors: "Validation failed: Title can't be blank, Title is invalid" },
{ type: :milestone, url: "https://api.github.com/repos/octocat/Hello-World/milestones/1", errors: "Validation failed: Title has already been taken" },
- { type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :issue, url: "https://api.github.com/repos/octocat/Hello-World/issues/1348", errors: "Validation failed: Title can't be blank, Title is too short (minimum is 0 characters)" },
- { type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Invalid Repository. Use user/repo format." },
{ type: :pull_request, url: "https://api.github.com/repos/octocat/Hello-World/pulls/1347", errors: "Validation failed: Validate branches Cannot Create: This merge request already exists: [\"New feature\"]" },
{ type: :wiki, errors: "Gitlab::Shell::Error" },
{ type: :release, url: 'https://api.github.com/repos/octocat/Hello-World/releases/2', errors: "Validation failed: Description can't be blank" }
diff --git a/spec/lib/gitlab/github_import/project_creator_spec.rb b/spec/lib/gitlab/github_import/project_creator_spec.rb
index ab06b7bc5bb..a73b1f4ff5d 100644
--- a/spec/lib/gitlab/github_import/project_creator_spec.rb
+++ b/spec/lib/gitlab/github_import/project_creator_spec.rb
@@ -33,7 +33,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.import_data.credentials).to eq(user: 'asdffg', password: nil)
end
- context 'when Github project is private' do
+ context 'when GitHub project is private' do
it 'sets project visibility to private' do
repo.private = true
@@ -43,7 +43,7 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
end
end
- context 'when Github project is public' do
+ context 'when GitHub project is public' do
before do
allow_any_instance_of(ApplicationSetting).to receive(:default_project_visibility).and_return(Gitlab::VisibilityLevel::INTERNAL)
end
@@ -56,5 +56,25 @@ describe Gitlab::GithubImport::ProjectCreator, lib: true do
expect(project.visibility_level).to eq(Gitlab::VisibilityLevel::INTERNAL)
end
end
+
+ context 'when GitHub project has wiki' do
+ it 'does not create the wiki repository' do
+ allow(repo).to receive(:has_wiki?).and_return(true)
+
+ project = service.execute
+
+ expect(project.wiki.repository_exists?).to eq false
+ end
+ end
+
+ context 'when GitHub project does not have wiki' do
+ it 'creates the wiki repository' do
+ allow(repo).to receive(:has_wiki?).and_return(false)
+
+ project = service.execute
+
+ expect(project.wiki.repository_exists?).to eq true
+ end
+ end
end
end
diff --git a/spec/lib/gitlab/identifier_spec.rb b/spec/lib/gitlab/identifier_spec.rb
new file mode 100644
index 00000000000..47d6f1007d1
--- /dev/null
+++ b/spec/lib/gitlab/identifier_spec.rb
@@ -0,0 +1,123 @@
+require 'spec_helper'
+
+describe Gitlab::Identifier do
+ let(:identifier) do
+ Class.new { include Gitlab::Identifier }.new
+ end
+
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:key) { create(:key, user: user) }
+
+ describe '#identify' do
+ context 'without an identifier' do
+ it 'identifies the user using a commit' do
+ expect(identifier).to receive(:identify_using_commit).
+ with(project, '123')
+
+ identifier.identify('', project, '123')
+ end
+ end
+
+ context 'with a user identifier' do
+ it 'identifies the user using a user ID' do
+ expect(identifier).to receive(:identify_using_user).
+ with("user-#{user.id}")
+
+ identifier.identify("user-#{user.id}", project, '123')
+ end
+ end
+
+ context 'with an SSH key identifier' do
+ it 'identifies the user using an SSH key ID' do
+ expect(identifier).to receive(:identify_using_ssh_key).
+ with("key-#{key.id}")
+
+ identifier.identify("key-#{key.id}", project, '123')
+ end
+ end
+ end
+
+ describe '#identify_using_commit' do
+ it "returns the User for an existing commit author's Email address" do
+ commit = double(:commit, author_email: user.email)
+
+ expect(project).to receive(:commit).with('123').and_return(commit)
+
+ expect(identifier.identify_using_commit(project, '123')).to eq(user)
+ end
+
+ it 'returns nil when no user could be found' do
+ allow(project).to receive(:commit).with('123').and_return(nil)
+
+ expect(identifier.identify_using_commit(project, '123')).to be_nil
+ end
+
+ it 'returns nil when the commit does not have an author Email' do
+ commit = double(:commit, author_email: nil)
+
+ expect(project).to receive(:commit).with('123').and_return(commit)
+
+ expect(identifier.identify_using_commit(project, '123')).to be_nil
+ end
+
+ it 'caches the found users per Email' do
+ commit = double(:commit, author_email: user.email)
+
+ expect(project).to receive(:commit).with('123').twice.and_return(commit)
+ expect(User).to receive(:find_by).once.and_call_original
+
+ 2.times do
+ expect(identifier.identify_using_commit(project, '123')).to eq(user)
+ end
+ end
+ end
+
+ describe '#identify_using_user' do
+ it 'returns the User for an existing ID in the identifier' do
+ found = identifier.identify_using_user("user-#{user.id}")
+
+ expect(found).to eq(user)
+ end
+
+ it 'returns nil for a non existing user ID' do
+ found = identifier.identify_using_user('user--1')
+
+ expect(found).to be_nil
+ end
+
+ it 'caches the found users per ID' do
+ expect(User).to receive(:find_by).once.and_call_original
+
+ 2.times do
+ found = identifier.identify_using_user("user-#{user.id}")
+
+ expect(found).to eq(user)
+ end
+ end
+ end
+
+ describe '#identify_using_ssh_key' do
+ it 'returns the User for an existing SSH key' do
+ found = identifier.identify_using_ssh_key("key-#{key.id}")
+
+ expect(found).to eq(user)
+ end
+
+ it 'returns nil for an invalid SSH key' do
+ found = identifier.identify_using_ssh_key('key--1')
+
+ expect(found).to be_nil
+ end
+
+ it 'caches the found users per key' do
+ expect(User).to receive(:find_by_ssh_key_id).once.and_call_original
+
+ 2.times do
+ found = identifier.identify_using_ssh_key("key-#{key.id}")
+
+ expect(found).to eq(user)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
new file mode 100644
index 00000000000..b8e7932eb4a
--- /dev/null
+++ b/spec/lib/gitlab/import_export/attribute_cleaner_spec.rb
@@ -0,0 +1,34 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::AttributeCleaner, lib: true do
+ let(:unsafe_hash) do
+ {
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99
+ }
+ end
+
+ let(:post_safe_hash) do
+ {
+ 'project_id' => 99,
+ 'user_id' => 99,
+ 'random_id_in_the_middle' => 99,
+ 'notid' => 99
+ }
+ end
+
+ it 'removes unwanted attributes from the hash' do
+ described_class.clean!(relation_hash: unsafe_hash)
+
+ expect(unsafe_hash).to eq(post_safe_hash)
+ end
+end
diff --git a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
index 2ba344092ce..ea65a5dfed1 100644
--- a/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
+++ b/spec/lib/gitlab/import_export/attribute_configuration_spec.rb
@@ -26,10 +26,11 @@ describe 'Import/Export attribute configuration', lib: true do
it 'has no new columns' do
relation_names.each do |relation_name|
relation_class = relation_class_for_name(relation_name)
+ relation_attributes = relation_class.new.attributes.keys
- expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class.to_s} to exist in safe_model_attributes"
+ expect(safe_model_attributes[relation_class.to_s]).not_to be_nil, "Expected exported class #{relation_class} to exist in safe_model_attributes"
- current_attributes = parsed_attributes(relation_name, relation_class.attribute_names)
+ current_attributes = parsed_attributes(relation_name, relation_attributes)
safe_attributes = safe_model_attributes[relation_class.to_s]
new_attributes = current_attributes - safe_attributes
diff --git a/spec/lib/gitlab/import_export/project.json b/spec/lib/gitlab/import_export/project.json
index 281f6cf1177..98323fe6be4 100644
--- a/spec/lib/gitlab/import_export/project.json
+++ b/spec/lib/gitlab/import_export/project.json
@@ -2232,6 +2232,31 @@
],
"milestones": [
{
+ "id": 1,
+ "title": "test milestone",
+ "project_id": 8,
+ "description": "test milestone",
+ "due_date": null,
+ "created_at": "2016-06-14T15:02:04.415Z",
+ "updated_at": "2016-06-14T15:02:04.415Z",
+ "state": "active",
+ "iid": 1,
+ "events": [
+ {
+ "id": 487,
+ "target_type": "Milestone",
+ "target_id": 1,
+ "title": null,
+ "data": null,
+ "project_id": 46,
+ "created_at": "2016-06-14T15:02:04.418Z",
+ "updated_at": "2016-06-14T15:02:04.418Z",
+ "action": 1,
+ "author_id": 18
+ }
+ ]
+ },
+ {
"id": 20,
"title": "v4.0",
"project_id": 5,
@@ -6918,6 +6943,7 @@
"note_events": true,
"build_events": true,
"category": "issue_tracker",
+ "type": "CustomIssueTrackerService",
"default": true,
"wiki_page_events": true
},
@@ -7372,5 +7398,16 @@
}
]
}
- ]
+ ],
+ "project_feature": {
+ "builds_access_level": 0,
+ "created_at": "2014-12-26T09:26:45.000Z",
+ "id": 2,
+ "issues_access_level": 0,
+ "merge_requests_access_level": 20,
+ "project_id": 4,
+ "snippets_access_level": 20,
+ "updated_at": "2016-09-23T11:58:28.000Z",
+ "wiki_access_level": 20
+ }
} \ No newline at end of file
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 feacb295231..7582a732cdf 100644
--- a/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_restorer_spec.rb
@@ -107,6 +107,18 @@ describe Gitlab::ImportExport::ProjectTreeRestorer, services: true do
expect(Label.first.label_links.first.target).not_to be_nil
end
+ it 'has a project feature' do
+ restored_project_json
+
+ expect(project.project_feature).not_to be_nil
+ end
+
+ it 'restores the correct service' do
+ restored_project_json
+
+ expect(CustomIssueTrackerService.first).not_to be_nil
+ end
+
context 'Merge requests' do
before do
restored_project_json
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 d891c2d0cc6..cf8f2200c57 100644
--- a/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
+++ b/spec/lib/gitlab/import_export/project_tree_saver_spec.rb
@@ -111,6 +111,10 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
expect(saved_project_json['issues'].first['label_links'].first['label']).not_to be_empty
end
+ it 'saves the correct service type' do
+ expect(saved_project_json['services'].first['type']).to eq('CustomIssueTrackerService')
+ end
+
it 'has project feature' do
project_feature = saved_project_json['project_feature']
expect(project_feature).not_to be_empty
@@ -161,6 +165,7 @@ describe Gitlab::ImportExport::ProjectTreeSaver, services: true do
commit_id: ci_pipeline.sha)
create(:event, target: milestone, project: project, action: Event::CREATED, author: user)
+ create(:service, project: project, type: 'CustomIssueTrackerService', category: 'issue_tracker')
project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
project.project_feature.update_attribute(:wiki_access_level, ProjectFeature::ENABLED)
diff --git a/spec/lib/gitlab/import_export/relation_factory_spec.rb b/spec/lib/gitlab/import_export/relation_factory_spec.rb
new file mode 100644
index 00000000000..3aa492a8ab1
--- /dev/null
+++ b/spec/lib/gitlab/import_export/relation_factory_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe Gitlab::ImportExport::RelationFactory, lib: true do
+ let(:project) { create(:empty_project) }
+ let(:members_mapper) { double('members_mapper').as_null_object }
+ let(:user) { create(:user) }
+ let(:created_object) do
+ described_class.create(relation_sym: relation_sym,
+ relation_hash: relation_hash,
+ members_mapper: members_mapper,
+ user: user,
+ project_id: project.id)
+ end
+
+ context 'hook object' do
+ let(:relation_sym) { :hooks }
+ let(:id) { 999 }
+ let(:service_id) { 99 }
+ let(:original_project_id) { 8 }
+ let(:token) { 'secret' }
+
+ let(:relation_hash) do
+ {
+ 'id' => id,
+ 'url' => 'https://example.json',
+ 'project_id' => original_project_id,
+ 'created_at' => '2016-08-12T09:41:03.462Z',
+ 'updated_at' => '2016-08-12T09:41:03.462Z',
+ 'service_id' => service_id,
+ 'push_events' => true,
+ 'issues_events' => false,
+ 'merge_requests_events' => true,
+ 'tag_push_events' => false,
+ 'note_events' => true,
+ 'enable_ssl_verification' => true,
+ 'build_events' => false,
+ 'wiki_page_events' => true,
+ 'token' => token
+ }
+ end
+
+ it 'does not have the original ID' do
+ expect(created_object.id).not_to eq(id)
+ end
+
+ it 'does not have the original service_id' do
+ expect(created_object.service_id).not_to eq(service_id)
+ end
+
+ it 'does not have the original project_id' do
+ expect(created_object.project_id).not_to eq(original_project_id)
+ end
+
+ it 'has the new project_id' do
+ expect(created_object.project_id).to eq(project.id)
+ end
+
+ it 'has a token' do
+ expect(created_object.token).to eq(token)
+ end
+
+ context 'original service exists' do
+ let(:service_id) { Service.create(project: project).id }
+
+ it 'does not have the original service_id' do
+ expect(created_object.service_id).not_to eq(service_id)
+ end
+ end
+ end
+
+ # Mocks an ActiveRecordish object with the dodgy columns
+ class FooModel
+ include ActiveModel::Model
+
+ def initialize(params)
+ params.each { |key, value| send("#{key}=", value) }
+ end
+
+ def values
+ instance_variables.map { |ivar| instance_variable_get(ivar) }
+ end
+ end
+
+ # `project_id`, `described_class.USER_REFERENCES`, noteable_id, target_id, and some project IDs are already
+ # re-assigned by described_class.
+ context 'Potentially hazardous foreign keys' do
+ let(:relation_sym) { :hazardous_foo_model }
+ let(:relation_hash) do
+ {
+ 'service_id' => 99,
+ 'moved_to_id' => 99,
+ 'namespace_id' => 99,
+ 'ci_id' => 99,
+ 'random_project_id' => 99,
+ 'random_id' => 99,
+ 'milestone_id' => 99,
+ 'project_id' => 99,
+ 'user_id' => 99,
+ }
+ end
+
+ class HazardousFooModel < FooModel
+ attr_accessor :service_id, :moved_to_id, :namespace_id, :ci_id, :random_project_id, :random_id, :milestone_id, :project_id
+ end
+
+ it 'does not preserve any foreign key IDs' do
+ expect(created_object.values).not_to include(99)
+ end
+ end
+
+ context 'Project references' do
+ let(:relation_sym) { :project_foo_model }
+ let(:relation_hash) do
+ Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES.map { |ref| { ref => 99 } }.inject(:merge)
+ end
+
+ class ProjectFooModel < FooModel
+ attr_accessor(*Gitlab::ImportExport::RelationFactory::PROJECT_REFERENCES)
+ end
+
+ it 'does not preserve any project foreign key IDs' do
+ expect(created_object.values).not_to include(99)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/ldap/adapter_spec.rb b/spec/lib/gitlab/ldap/adapter_spec.rb
index 0600893f4cf..563c074017a 100644
--- a/spec/lib/gitlab/ldap/adapter_spec.rb
+++ b/spec/lib/gitlab/ldap/adapter_spec.rb
@@ -73,17 +73,33 @@ describe Gitlab::LDAP::Adapter, lib: true do
describe '#dn_matches_filter?' do
subject { adapter.dn_matches_filter?(:dn, :filter) }
+ context "when the search result is non-empty" do
+ before { allow(adapter).to receive(:ldap_search).and_return([:foo]) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context "when the search result is empty" do
+ before { allow(adapter).to receive(:ldap_search).and_return([]) }
+
+ it { is_expected.to be_falsey }
+ end
+ end
+
+ describe '#ldap_search' do
+ subject { adapter.ldap_search(base: :dn, filter: :filter) }
+
context "when the search is successful" do
context "and the result is non-empty" do
before { allow(ldap).to receive(:search).and_return([:foo]) }
- it { is_expected.to be_truthy }
+ it { is_expected.to eq [:foo] }
end
context "and the result is empty" do
before { allow(ldap).to receive(:search).and_return([]) }
- it { is_expected.to be_falsey }
+ it { is_expected.to eq [] }
end
end
@@ -95,7 +111,22 @@ describe Gitlab::LDAP::Adapter, lib: true do
)
end
- it { is_expected.to be_falsey }
+ it { is_expected.to eq [] }
+ end
+
+ context "when the search raises an LDAP exception" do
+ before do
+ allow(ldap).to receive(:search) { raise Net::LDAP::Error, "some error" }
+ allow(Rails.logger).to receive(:warn)
+ end
+
+ it { is_expected.to eq [] }
+
+ it 'logs the error' do
+ subject
+ expect(Rails.logger).to have_received(:warn).with(
+ "LDAP search raised exception Net::LDAP::Error: some error")
+ end
end
end
end
diff --git a/spec/lib/gitlab/lfs_token_spec.rb b/spec/lib/gitlab/lfs_token_spec.rb
index 9f04f67e0a8..e9c1163e22a 100644
--- a/spec/lib/gitlab/lfs_token_spec.rb
+++ b/spec/lib/gitlab/lfs_token_spec.rb
@@ -1,10 +1,10 @@
require 'spec_helper'
describe Gitlab::LfsToken, lib: true do
- describe '#generate and #value' do
+ describe '#token' do
shared_examples 'an LFS token generator' do
it 'returns a randomly generated token' do
- token = handler.generate
+ token = handler.token
expect(token).not_to be_nil
expect(token).to be_a String
@@ -12,9 +12,9 @@ describe Gitlab::LfsToken, lib: true do
end
it 'returns the correct token based on the key' do
- token = handler.generate
+ token = handler.token
- expect(handler.value).to eq(token)
+ expect(handler.token).to eq(token)
end
end
diff --git a/spec/lib/gitlab/redis_spec.rb b/spec/lib/gitlab/redis_spec.rb
index cb54c020b31..74ff85e132a 100644
--- a/spec/lib/gitlab/redis_spec.rb
+++ b/spec/lib/gitlab/redis_spec.rb
@@ -88,6 +88,34 @@ describe Gitlab::Redis do
end
end
+ describe '.with' do
+ before { clear_pool }
+ after { clear_pool }
+
+ context 'when running not on sidekiq workers' do
+ before { allow(Sidekiq).to receive(:server?).and_return(false) }
+
+ it 'instantiates a connection pool with size 5' do
+ expect(ConnectionPool).to receive(:new).with(size: 5).and_call_original
+
+ described_class.with { |_redis| true }
+ end
+ end
+
+ context 'when running on sidekiq workers' do
+ before do
+ allow(Sidekiq).to receive(:server?).and_return(true)
+ allow(Sidekiq).to receive(:options).and_return({ concurrency: 18 })
+ end
+
+ it 'instantiates a connection pool with a size based on the concurrency of the worker' do
+ expect(ConnectionPool).to receive(:new).with(size: 18 + 5).and_call_original
+
+ described_class.with { |_redis| true }
+ end
+ end
+ end
+
describe '#raw_config_hash' do
it 'returns default redis url when no config file is present' do
expect(subject).to receive(:fetch_config) { false }
@@ -114,4 +142,10 @@ describe Gitlab::Redis do
rescue NameError
# raised if @_raw_config was not set; ignore
end
+
+ def clear_pool
+ described_class.remove_instance_variable(:@pool)
+ rescue NameError
+ # raised if @pool was not set; ignore
+ end
end
diff --git a/spec/lib/gitlab/template/issue_template_spec.rb b/spec/lib/gitlab/template/issue_template_spec.rb
index f770857e958..d2d334e6413 100644
--- a/spec/lib/gitlab/template/issue_template_spec.rb
+++ b/spec/lib/gitlab/template/issue_template_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Template::IssueTemplate do
let(:file_path_3) { '.gitlab/issue_templates/feature_proposal.md' }
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
@@ -53,7 +53,7 @@ describe Gitlab::Template::IssueTemplate do
context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do
templates = subject.by_category('', empty_project)
@@ -78,7 +78,7 @@ describe Gitlab::Template::IssueTemplate do
context "when repo is empty" do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do
issue_template = subject.new('.gitlab/issue_templates/not_existent.md', empty_project)
diff --git a/spec/lib/gitlab/template/merge_request_template_spec.rb b/spec/lib/gitlab/template/merge_request_template_spec.rb
index bb0f68043fa..ddf68c4cf78 100644
--- a/spec/lib/gitlab/template/merge_request_template_spec.rb
+++ b/spec/lib/gitlab/template/merge_request_template_spec.rb
@@ -10,7 +10,7 @@ describe Gitlab::Template::MergeRequestTemplate do
let(:file_path_3) { '.gitlab/merge_request_templates/feature_proposal.md' }
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
project.repository.commit_file(user, file_path_1, "something valid", "test 3", "master", false)
project.repository.commit_file(user, file_path_2, "template_test", "test 1", "master", false)
project.repository.commit_file(user, file_path_3, "feature_proposal", "test 2", "master", false)
@@ -53,7 +53,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context 'when repo is bare or empty' do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "returns empty array" do
templates = subject.by_category('', empty_project)
@@ -78,7 +78,7 @@ describe Gitlab::Template::MergeRequestTemplate do
context "when repo is empty" do
let(:empty_project) { create(:empty_project) }
- before { empty_project.team.add_user(user, Gitlab::Access::MASTER) }
+ before { empty_project.add_user(user, Gitlab::Access::MASTER) }
it "raises file not found" do
issue_template = subject.new('.gitlab/merge_request_templates/not_existent.md', empty_project)
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 6c7fa7e7c15..b5b685da904 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -1,8 +1,16 @@
require 'spec_helper'
describe Gitlab::Workhorse, lib: true do
- let(:project) { create(:project) }
- let(:subject) { Gitlab::Workhorse }
+ let(:project) { create(:project) }
+ let(:repository) { project.repository }
+
+ def decode_workhorse_header(array)
+ key, value = array
+ command, encoded_params = value.split(":")
+ params = JSON.parse(Base64.urlsafe_decode64(encoded_params))
+
+ [key, command, params]
+ end
describe ".send_git_archive" do
context "when the repository doesn't have an archive file path" do
@@ -11,11 +19,37 @@ describe Gitlab::Workhorse, lib: true do
end
it "raises an error" do
- expect { subject.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
+ expect { described_class.send_git_archive(project.repository, ref: "master", format: "zip") }.to raise_error(RuntimeError)
end
end
end
+ describe '.send_git_patch' do
+ let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
+ subject { described_class.send_git_patch(repository, diff_refs) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
+ end
+
+ describe '.send_git_diff' do
+ let(:diff_refs) { double(base_sha: "base", head_sha: "head") }
+ subject { described_class.send_git_patch(repository, diff_refs) }
+
+ it 'sets the header correctly' do
+ key, command, params = decode_workhorse_header(subject)
+
+ expect(key).to eq("Gitlab-Workhorse-Send-Data")
+ expect(command).to eq("git-format-patch")
+ expect(params).to eq("RepoPath" => repository.path_to_repo, "ShaFrom" => "base", "ShaTo" => "head")
+ end
+ end
+
describe ".secret" do
subject { described_class.secret }
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 0363bc74939..0e4130e8a3a 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -402,7 +402,7 @@ describe Notify do
describe 'project access requested' do
context 'for a project in a user namespace' do
- let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:project) { create(:project, :public).tap { |p| p.team << [p.owner, :master, p.owner] } }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -429,7 +429,7 @@ describe Notify do
context 'for a project in a group' do
let(:group_owner) { create(:user) }
let(:group) { create(:group).tap { |g| g.add_owner(group_owner) } }
- let(:project) { create(:project, namespace: group) }
+ let(:project) { create(:project, :public, namespace: group) }
let(:user) { create(:user) }
let(:project_member) do
project.request_access(user)
@@ -492,21 +492,22 @@ describe Notify do
end
end
- def invite_to_project(project:, email:, inviter:)
- Member.add_user(
- project.project_members,
- 'toto@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: inviter
+ def invite_to_project(project, inviter:)
+ create(
+ :project_member,
+ :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto@example.com',
+ user: nil,
+ created_by: inviter
)
-
- project.project_members.invite.last
end
describe 'project invitation' do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
- let(:project_member) { invite_to_project(project: project, email: 'toto@example.com', inviter: master) }
+ let(:project_member) { invite_to_project(project, inviter: master) }
subject { Notify.member_invited_email('project', project_member.id, project_member.invite_token) }
@@ -525,10 +526,10 @@ describe Notify do
describe 'project invitation accepted' do
let(:project) { create(:project) }
- let(:invited_user) { create(:user) }
+ let(:invited_user) { create(:user, name: 'invited user') }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
- invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee = invite_to_project(project, inviter: master)
invitee.accept_invite!(invited_user)
invitee
end
@@ -552,7 +553,7 @@ describe Notify do
let(:project) { create(:project) }
let(:master) { create(:user).tap { |u| project.team << [u, :master] } }
let(:project_member) do
- invitee = invite_to_project(project: project, email: 'toto@example.com', inviter: master)
+ invitee = invite_to_project(project, inviter: master)
invitee.decline_invite!
invitee
end
@@ -744,21 +745,22 @@ describe Notify do
end
end
- def invite_to_group(group:, email:, inviter:)
- Member.add_user(
- group.group_members,
- 'toto@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: inviter
+ def invite_to_group(group, inviter:)
+ create(
+ :group_member,
+ :developer,
+ group: group,
+ invite_token: '1234',
+ invite_email: 'toto@example.com',
+ user: nil,
+ created_by: inviter
)
-
- group.group_members.invite.last
end
describe 'group invitation' do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
- let(:group_member) { invite_to_group(group: group, email: 'toto@example.com', inviter: owner) }
+ let(:group_member) { invite_to_group(group, inviter: owner) }
subject { Notify.member_invited_email('group', group_member.id, group_member.invite_token) }
@@ -777,10 +779,10 @@ describe Notify do
describe 'group invitation accepted' do
let(:group) { create(:group) }
- let(:invited_user) { create(:user) }
+ let(:invited_user) { create(:user, name: 'invited user') }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
- invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee = invite_to_group(group, inviter: owner)
invitee.accept_invite!(invited_user)
invitee
end
@@ -804,7 +806,7 @@ describe Notify do
let(:group) { create(:group) }
let(:owner) { create(:user).tap { |u| group.add_user(u, Gitlab::Access::OWNER) } }
let(:group_member) do
- invitee = invite_to_group(group: group, email: 'toto@example.com', inviter: owner)
+ invitee = invite_to_group(group, inviter: owner)
invitee.decline_invite!
invitee
end
@@ -829,6 +831,7 @@ describe Notify do
let(:user) { create(:user, email: 'old-email@mail.com') }
before do
+ stub_config_setting(email_subject_suffix: 'A Nice Suffix')
perform_enqueued_jobs do
user.email = "new-email@mail.com"
user.save
@@ -845,7 +848,7 @@ describe Notify do
end
it 'has the correct subject' do
- is_expected.to have_subject "Confirmation instructions"
+ is_expected.to have_subject /^Confirmation instructions/
end
it 'includes a link to the site' do
diff --git a/spec/mailers/shared/notify.rb b/spec/mailers/shared/notify.rb
index 56872da9a8f..3956d05060b 100644
--- a/spec/mailers/shared/notify.rb
+++ b/spec/mailers/shared/notify.rb
@@ -37,6 +37,16 @@ shared_examples 'an email sent from GitLab' do
reply_to = subject.header[:reply_to].addresses
expect(reply_to).to eq([gitlab_sender_reply_to])
end
+
+ context 'when custom suffix for email subject is set' do
+ before do
+ stub_config_setting(email_subject_suffix: 'A Nice Suffix')
+ end
+
+ it 'ends the subject with the suffix' do
+ is_expected.to have_subject /\ \| A Nice Suffix$/
+ end
+ end
end
shared_examples 'an email that contains a header with author username' do
@@ -169,8 +179,9 @@ shared_examples 'it should show Gmail Actions View Commit link' do
end
shared_examples 'an unsubscribeable thread' do
- it 'has a List-Unsubscribe header' do
+ it 'has a List-Unsubscribe header in the correct format' do
is_expected.to have_header 'List-Unsubscribe', /unsubscribe/
+ is_expected.to have_header 'List-Unsubscribe', /^<.+>$/
end
it { is_expected.to have_body_text /unsubscribe/ }
diff --git a/spec/models/abuse_report_spec.rb b/spec/models/abuse_report_spec.rb
index 305f8bc88cc..c4486a32082 100644
--- a/spec/models/abuse_report_spec.rb
+++ b/spec/models/abuse_report_spec.rb
@@ -9,6 +9,10 @@ RSpec.describe AbuseReport, type: :model do
describe 'associations' do
it { is_expected.to belong_to(:reporter).class_name('User') }
it { is_expected.to belong_to(:user) }
+
+ it "aliases reporter to author" do
+ expect(subject.author).to be(subject.reporter)
+ end
end
describe 'validations' do
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index e7864b7ad33..ae185de9ca3 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -39,8 +39,8 @@ describe Ci::Build, models: true do
end
end
- describe '#ignored?' do
- subject { build.ignored? }
+ describe '#failed_but_allowed?' do
+ subject { build.failed_but_allowed? }
context 'when build is not allowed to fail' do
before do
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 2f1baff5d66..80c2a1bc7a9 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -7,7 +7,11 @@ describe CommitStatus, models: true do
create(:ci_pipeline, project: project, sha: project.commit.id)
end
- let(:commit_status) { create(:commit_status, pipeline: pipeline) }
+ let(:commit_status) { create_status }
+
+ def create_status(args = {})
+ create(:commit_status, args.merge(pipeline: pipeline))
+ end
it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
@@ -125,32 +129,53 @@ describe CommitStatus, models: true do
describe '.latest' do
subject { CommitStatus.latest.order(:id) }
- before do
- @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'cc', status: 'success'
- @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'bb', status: 'success'
- @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'success'
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running'),
+ create_status(name: 'cc', ref: 'cc', status: 'pending'),
+ create_status(name: 'aa', ref: 'cc', status: 'success'),
+ create_status(name: 'cc', ref: 'bb', status: 'success'),
+ create_status(name: 'aa', ref: 'bb', status: 'success')]
end
it 'returns unique statuses' do
- is_expected.to eq([@commit4, @commit5])
+ is_expected.to eq(statuses.values_at(3, 4))
end
end
describe '.running_or_pending' do
subject { CommitStatus.running_or_pending.order(:id) }
- before do
- @commit1 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: 'bb', status: 'running'
- @commit2 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'cc', ref: 'cc', status: 'pending'
- @commit3 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'aa', ref: nil, status: 'success'
- @commit4 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'dd', ref: nil, status: 'failed'
- @commit5 = FactoryGirl.create :commit_status, pipeline: pipeline, name: 'ee', ref: nil, status: 'canceled'
+ let(:statuses) do
+ [create_status(name: 'aa', ref: 'bb', status: 'running'),
+ create_status(name: 'cc', ref: 'cc', status: 'pending'),
+ create_status(name: 'aa', ref: nil, status: 'success'),
+ create_status(name: 'dd', ref: nil, status: 'failed'),
+ create_status(name: 'ee', ref: nil, status: 'canceled')]
end
it 'returns statuses that are running or pending' do
- is_expected.to eq([@commit1, @commit2])
+ is_expected.to eq(statuses.values_at(0, 1))
+ end
+ end
+
+ describe '.exclude_ignored' do
+ subject { CommitStatus.exclude_ignored.order(:id) }
+
+ let(:statuses) do
+ [create_status(when: 'manual', status: 'skipped'),
+ create_status(when: 'manual', status: 'success'),
+ create_status(when: 'manual', status: 'failed'),
+ create_status(when: 'on_failure', status: 'skipped'),
+ create_status(when: 'on_failure', status: 'success'),
+ create_status(when: 'on_failure', status: 'failed'),
+ create_status(allow_failure: true, status: 'success'),
+ create_status(allow_failure: true, status: 'failed'),
+ create_status(allow_failure: false, status: 'success'),
+ create_status(allow_failure: false, status: 'failed')]
+ end
+
+ it 'returns statuses without what we want to ignore' do
+ is_expected.to eq(statuses.values_at(1, 2, 4, 5, 6, 8, 9))
end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
new file mode 100644
index 00000000000..15cd3a7ed70
--- /dev/null
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -0,0 +1,181 @@
+require 'spec_helper'
+
+describe CacheMarkdownField do
+ CacheMarkdownField::CACHING_CLASSES << "ThingWithMarkdownFields"
+
+ # The minimum necessary ActiveModel to test this concern
+ class ThingWithMarkdownFields
+ include ActiveModel::Model
+ include ActiveModel::Dirty
+
+ include ActiveModel::Serialization
+
+ class_attribute :attribute_names
+ self.attribute_names = []
+
+ def attributes
+ attribute_names.each_with_object({}) do |name, hsh|
+ hsh[name.to_s] = send(name)
+ end
+ end
+
+ extend ActiveModel::Callbacks
+ define_model_callbacks :save
+
+ include CacheMarkdownField
+ cache_markdown_field :foo
+ cache_markdown_field :baz, pipeline: :single_line
+
+ def self.add_attr(attr_name)
+ self.attribute_names += [attr_name]
+ define_attribute_methods(attr_name)
+ attr_reader(attr_name)
+ define_method("#{attr_name}=") do |val|
+ send("#{attr_name}_will_change!") unless val == send(attr_name)
+ instance_variable_set("@#{attr_name}", val)
+ end
+ end
+
+ [:foo, :foo_html, :bar, :baz, :baz_html].each do |attr_name|
+ add_attr(attr_name)
+ end
+
+ def initialize(*)
+ super
+
+ # Pretend new is load
+ clear_changes_information
+ end
+
+ def save
+ run_callbacks :save do
+ changes_applied
+ end
+ end
+ end
+
+ CacheMarkdownField::CACHING_CLASSES.delete("ThingWithMarkdownFields")
+
+ def thing_subclass(new_attr)
+ Class.new(ThingWithMarkdownFields) { add_attr(new_attr) }
+ end
+
+ let(:markdown) { "`Foo`" }
+ let(:html) { "<p><code>Foo</code></p>" }
+
+ let(:updated_markdown) { "`Bar`" }
+ let(:updated_html) { "<p><code>Bar</code></p>" }
+
+ subject { ThingWithMarkdownFields.new(foo: markdown, foo_html: html) }
+
+ describe ".attributes" do
+ it "excludes cache attributes" do
+ expect(thing_subclass(:qux).new.attributes.keys.sort).to eq(%w[bar baz foo qux])
+ end
+ end
+
+ describe ".cache_markdown_field" do
+ it "refuses to allow untracked classes" do
+ expect { thing_subclass(:qux).__send__(:cache_markdown_field, :qux) }.to raise_error(RuntimeError)
+ end
+ end
+
+ context "an unchanged markdown field" do
+ before do
+ subject.foo = subject.foo
+ subject.save
+ end
+
+ it { expect(subject.foo).to eq(markdown) }
+ it { expect(subject.foo_html).to eq(html) }
+ it { expect(subject.foo_html_changed?).not_to be_truthy }
+ end
+
+ context "a changed markdown field" do
+ before do
+ subject.foo = updated_markdown
+ subject.save
+ end
+
+ it { expect(subject.foo_html).to eq(updated_html) }
+ end
+
+ context "a non-markdown field changed" do
+ before do
+ subject.bar = "OK"
+ subject.save
+ end
+
+ it { expect(subject.bar).to eq("OK") }
+ it { expect(subject.foo).to eq(markdown) }
+ it { expect(subject.foo_html).to eq(html) }
+ end
+
+ describe '#banzai_render_context' do
+ it "sets project to nil if the object lacks a project" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).to have_key(:project)
+ expect(context[:project]).to be_nil
+ end
+
+ it "excludes author if the object lacks an author" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).not_to have_key(:author)
+ end
+
+ it "raises if the context for an unrecognised field is requested" do
+ expect{subject.banzai_render_context(:not_found)}.to raise_error(ArgumentError)
+ end
+
+ it "includes the pipeline" do
+ context = subject.banzai_render_context(:baz)
+ expect(context[:pipeline]).to eq(:single_line)
+ end
+
+ it "returns copies of the context template" do
+ template = subject.cached_markdown_fields[:baz]
+ copy = subject.banzai_render_context(:baz)
+ expect(copy).not_to be(template)
+ end
+
+ context "with a project" do
+ subject { thing_subclass(:project).new(foo: markdown, foo_html: html, project: :project) }
+
+ it "sets the project in the context" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).to have_key(:project)
+ expect(context[:project]).to eq(:project)
+ end
+
+ it "invalidates the cache when project changes" do
+ subject.project = :new_project
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ subject.save
+
+ expect(subject.foo_html).to eq(updated_html)
+ expect(subject.baz_html).to eq(updated_html)
+ end
+ end
+
+ context "with an author" do
+ subject { thing_subclass(:author).new(foo: markdown, foo_html: html, author: :author) }
+
+ it "sets the author in the context" do
+ context = subject.banzai_render_context(:foo)
+ expect(context).to have_key(:author)
+ expect(context[:author]).to eq(:author)
+ end
+
+ it "invalidates the cache when author changes" do
+ subject.author = :new_author
+ allow(Banzai::Renderer).to receive(:cacheless_render_field).and_return(updated_html)
+
+ subject.save
+
+ expect(subject.foo_html).to eq(updated_html)
+ expect(subject.baz_html).to eq(updated_html)
+ end
+ end
+ end
+end
diff --git a/spec/models/concerns/has_status_spec.rb b/spec/models/concerns/has_status_spec.rb
index e118432d098..87bffbdc54e 100644
--- a/spec/models/concerns/has_status_spec.rb
+++ b/spec/models/concerns/has_status_spec.rb
@@ -1,26 +1,17 @@
require 'spec_helper'
describe HasStatus do
- before do
- @object = Object.new
- @object.extend(HasStatus::ClassMethods)
- end
-
describe '.status' do
- before do
- allow(@object).to receive(:all).and_return(CommitStatus.where(id: statuses))
- end
-
- subject { @object.status }
+ subject { CommitStatus.status }
shared_examples 'build status summary' do
context 'all successful' do
- let(:statuses) { Array.new(2) { create(type, status: :success) } }
+ let!(:statuses) { Array.new(2) { create(type, status: :success) } }
it { is_expected.to eq 'success' }
end
context 'at least one failed' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :failed)]
end
@@ -28,7 +19,7 @@ describe HasStatus do
end
context 'at least one running' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :running)]
end
@@ -36,7 +27,7 @@ describe HasStatus do
end
context 'at least one pending' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :pending)]
end
@@ -44,7 +35,7 @@ describe HasStatus do
end
context 'success and failed but allowed to fail' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success),
create(type, status: :failed, allow_failure: true)]
end
@@ -53,12 +44,15 @@ describe HasStatus do
end
context 'one failed but allowed to fail' do
- let(:statuses) { [create(type, status: :failed, allow_failure: true)] }
+ let!(:statuses) do
+ [create(type, status: :failed, allow_failure: true)]
+ end
+
it { is_expected.to eq 'success' }
end
context 'success and canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success), create(type, status: :canceled)]
end
@@ -66,7 +60,7 @@ describe HasStatus do
end
context 'one failed and one canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :failed), create(type, status: :canceled)]
end
@@ -74,7 +68,7 @@ describe HasStatus do
end
context 'one failed but allowed to fail and one canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :failed, allow_failure: true),
create(type, status: :canceled)]
end
@@ -83,7 +77,7 @@ describe HasStatus do
end
context 'one running one canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :running), create(type, status: :canceled)]
end
@@ -91,14 +85,15 @@ describe HasStatus do
end
context 'all canceled' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :canceled), create(type, status: :canceled)]
end
+
it { is_expected.to eq 'canceled' }
end
context 'success and canceled but allowed to fail' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success),
create(type, status: :canceled, allow_failure: true)]
end
@@ -107,7 +102,7 @@ describe HasStatus do
end
context 'one finished and second running but allowed to fail' do
- let(:statuses) do
+ let!(:statuses) do
[create(type, status: :success),
create(type, status: :running, allow_failure: true)]
end
@@ -118,11 +113,13 @@ describe HasStatus do
context 'ci build statuses' do
let(:type) { :ci_build }
+
it_behaves_like 'build status summary'
end
context 'generic commit statuses' do
let(:type) { :generic_commit_status }
+
it_behaves_like 'build status summary'
end
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index 549b0042038..132858950d5 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -1,18 +1,27 @@
require 'spec_helper'
describe Mentionable do
- include Mentionable
+ class Example
+ include Mentionable
- def author
- nil
+ attr_accessor :project, :message
+ attr_mentionable :message
+
+ def author
+ nil
+ end
end
describe 'references' do
let(:project) { create(:project) }
+ let(:mentionable) { Example.new }
it 'excludes JIRA references' do
allow(project).to receive_messages(jira_tracker?: true)
- expect(referenced_mentionables(project, 'JIRA-123')).to be_empty
+
+ mentionable.project = project
+ mentionable.message = 'JIRA-123'
+ expect(mentionable.referenced_mentionables).to be_empty
end
end
end
@@ -39,9 +48,8 @@ describe Issue, "Mentionable" do
let(:user) { create(:user) }
def referenced_issues(current_user)
- text = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
-
- issue.referenced_mentionables(current_user, text)
+ issue.title = "#{private_issue.to_reference(project)} and #{public_issue.to_reference}"
+ issue.referenced_mentionables(current_user)
end
context 'when the current user can see the issue' do
diff --git a/spec/models/cycle_analytics/summary_spec.rb b/spec/models/cycle_analytics/summary_spec.rb
index 743bc2da33f..9d67bc82cba 100644
--- a/spec/models/cycle_analytics/summary_spec.rb
+++ b/spec/models/cycle_analytics/summary_spec.rb
@@ -34,6 +34,12 @@ describe CycleAnalytics::Summary, models: true do
expect(subject.commits).to eq(0)
end
+
+ it "finds a large (> 100) snumber of commits if present" do
+ Timecop.freeze(5.days.from_now) { create_commit("Test message", project, user, 'master', count: 100) }
+
+ expect(subject.commits).to eq(100)
+ end
end
describe "#deploys" do
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 8600eb4d2c4..af5002487cc 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -173,13 +173,11 @@ describe Event, models: true do
it 'updates the project' do
project.update(last_activity_at: 1.year.ago)
- expect_any_instance_of(Gitlab::ExclusiveLease).
- to receive(:try_obtain).and_return(true)
+ create_event(project, project.owner)
- expect(project).to receive(:update_column).
- with(:last_activity_at, a_kind_of(Time))
+ project.reload
- create_event(project, project.owner)
+ project.last_activity_at <= 1.minute.ago
end
end
end
diff --git a/spec/models/forked_project_link_spec.rb b/spec/models/forked_project_link_spec.rb
index 9c81d159cdf..1863581f57b 100644
--- a/spec/models/forked_project_link_spec.rb
+++ b/spec/models/forked_project_link_spec.rb
@@ -6,6 +6,7 @@ describe ForkedProjectLink, "add link on fork" do
let(:user) { create(:user, namespace: namespace) }
before do
+ create(:project_member, :reporter, user: user, project: project_from)
@project_to = fork_project(project_from, user)
end
diff --git a/spec/models/issue_spec.rb b/spec/models/issue_spec.rb
index 3259f795296..3b8b743af2d 100644
--- a/spec/models/issue_spec.rb
+++ b/spec/models/issue_spec.rb
@@ -494,7 +494,7 @@ describe Issue, models: true do
context 'with an admin user' do
let(:project) { create(:empty_project) }
- let(:user) { create(:user, admin: true) }
+ let(:user) { create(:admin) }
it 'returns true for a regular issue' do
issue = build(:issue, project: project)
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 0b1634f654a..485121701af 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -57,7 +57,7 @@ describe Member, models: true do
describe 'Scopes & finders' do
before do
- project = create(:empty_project)
+ project = create(:empty_project, :public)
group = create(:group)
@owner_user = create(:user).tap { |u| group.add_owner(u) }
@owner = group.members.find_by(user_id: @owner_user.id)
@@ -74,22 +74,17 @@ describe Member, models: true do
@blocked_master = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::MASTER)
@blocked_developer = project.members.find_by(user_id: @blocked_user.id, access_level: Gitlab::Access::DEVELOPER)
- Member.add_user(
- project.members,
- 'toto1@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: @master_user
- )
- @invited_member = project.members.invite.find_by_invite_email('toto1@example.com')
+ @invited_member = create(:project_member, :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto1@example.com')
accepted_invite_user = build(:user, state: :active)
- Member.add_user(
- project.members,
- 'toto2@example.com',
- Gitlab::Access::DEVELOPER,
- current_user: @master_user
- )
- @accepted_invite_member = project.members.invite.find_by_invite_email('toto2@example.com').tap { |u| u.accept_invite!(accepted_invite_user) }
+ @accepted_invite_member = create(:project_member, :developer,
+ project: project,
+ invite_token: '1234',
+ invite_email: 'toto2@example.com').
+ tap { |u| u.accept_invite!(accepted_invite_user) }
requested_user = create(:user).tap { |u| project.request_access(u) }
@requested_member = project.requesters.find_by(user_id: requested_user.id)
@@ -176,39 +171,209 @@ describe Member, models: true do
it { is_expected.to respond_to(:user_email) }
end
- describe ".add_user" do
- let!(:user) { create(:user) }
- let(:project) { create(:project) }
+ describe '.add_user' do
+ %w[project group].each do |source_type|
+ context "when source is a #{source_type}" do
+ let!(:source) { create(source_type, :public) }
+ let!(:user) { create(:user) }
+ let!(:admin) { create(:admin) }
- context "when called with a user id" do
- it "adds the user as a member" do
- Member.add_user(project.project_members, user.id, ProjectMember::MASTER)
+ it 'returns a <Source>Member object' do
+ member = described_class.add_user(source, user, :master)
- expect(project.users).to include(user)
- end
- end
+ expect(member).to be_a "#{source_type.classify}Member".constantize
+ expect(member).to be_persisted
+ end
- context "when called with a user object" do
- it "adds the user as a member" do
- Member.add_user(project.project_members, user, ProjectMember::MASTER)
+ it 'sets members.created_by to the given current_user' do
+ member = described_class.add_user(source, user, :master, current_user: admin)
- expect(project.users).to include(user)
- end
- end
+ expect(member.created_by).to eq(admin)
+ end
- context "when called with a known user email" do
- it "adds the user as a member" do
- Member.add_user(project.project_members, user.email, ProjectMember::MASTER)
+ it 'sets members.expires_at to the given expires_at' do
+ member = described_class.add_user(source, user, :master, expires_at: Date.new(2016, 9, 22))
- expect(project.users).to include(user)
- end
- end
+ expect(member.expires_at).to eq(Date.new(2016, 9, 22))
+ end
+
+ described_class.access_levels.each do |sym_key, int_access_level|
+ it "accepts the :#{sym_key} symbol as access level" do
+ expect(source.users).not_to include(user)
+
+ member = described_class.add_user(source, user.id, sym_key)
+
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+
+ it "accepts the #{int_access_level} integer as access level" do
+ expect(source.users).not_to include(user)
+
+ member = described_class.add_user(source, user.id, int_access_level)
+
+ expect(member.access_level).to eq(int_access_level)
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'with no current_user' do
+ context 'when called with a known user id' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user.id, :master)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with an unknown user id' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, 42, :master)
+
+ expect(source.users.reload).not_to include(user)
+ end
+ end
+
+ context 'when called with a user object' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user, :master)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'adds the requester as a member' do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+ expect { described_class.add_user(source, user, :master) }.
+ to raise_error(Gitlab::Access::AccessDeniedError)
+
+ expect(source.users.reload).not_to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_truthy
+ end
+ end
+
+ context 'when called with a known user email' do
+ it 'adds the user as a member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user.email, :master)
+
+ expect(source.users.reload).to include(user)
+ end
+ end
+
+ context 'when called with an unknown user email' do
+ it 'creates an invited member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, 'user@example.com', :master)
+
+ expect(source.members.invite.pluck(:invite_email)).to include('user@example.com')
+ end
+ end
+ end
+
+ context 'when current_user can update member' do
+ it 'creates the member' do
+ expect(source.users).not_to include(user)
+
+ described_class.add_user(source, user, :master, current_user: admin)
+
+ expect(source.users.reload).to include(user)
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'adds the requester as a member' do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+ described_class.add_user(source, user, :master, current_user: admin)
+
+ expect(source.users.reload).to include(user)
+ expect(source.requesters.reload.exists?(user_id: user)).to be_falsy
+ end
+ end
+ end
+
+ context 'when current_user cannot update member' do
+ it 'does not create the member' do
+ expect(source.users).not_to include(user)
+
+ member = described_class.add_user(source, user, :master, current_user: user)
+
+ expect(source.users.reload).not_to include(user)
+ expect(member).not_to be_persisted
+ end
+
+ context 'when called with a requester user object' do
+ before do
+ source.request_access(user)
+ end
+
+ it 'does not destroy the requester' do
+ expect(source.users).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+
+ described_class.add_user(source, user, :master, current_user: user)
+
+ expect(source.users.reload).not_to include(user)
+ expect(source.requesters.exists?(user_id: user)).to be_truthy
+ end
+ end
+ end
+
+ context 'when member already exists' do
+ before do
+ source.add_user(user, :developer)
+ end
+
+ context 'with no current_user' do
+ it 'updates the member' do
+ expect(source.users).to include(user)
+
+ described_class.add_user(source, user, :master)
+
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'when current_user can update member' do
+ it 'updates the member' do
+ expect(source.users).to include(user)
+
+ described_class.add_user(source, user, :master, current_user: admin)
+
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::MASTER)
+ end
+ end
+
+ context 'when current_user cannot update member' do
+ it 'does not update the member' do
+ expect(source.users).to include(user)
- context "when called with an unknown user email" do
- it "adds a member invite" do
- Member.add_user(project.project_members, "user@example.com", ProjectMember::MASTER)
+ described_class.add_user(source, user, :master, current_user: user)
- expect(project.project_members.invite.pluck(:invite_email)).to include("user@example.com")
+ expect(source.members.find_by(user_id: user).access_level).to eq(Gitlab::Access::DEVELOPER)
+ end
+ end
+ end
end
end
end
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index 56fa7fa6134..370aeb9e0a9 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -1,6 +1,33 @@
require 'spec_helper'
describe GroupMember, models: true do
+ describe '.access_level_roles' do
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(described_class.access_level_roles).to eq(Gitlab::Access.options_with_owner)
+ end
+ end
+
+ describe '.access_levels' do
+ it 'returns Gitlab::Access.options_with_owner' do
+ expect(described_class.access_levels).to eq(Gitlab::Access.sym_options_with_owner)
+ end
+ end
+
+ describe '.add_users_to_group' do
+ it 'adds the given users to the given group' do
+ group = create(:group)
+ users = create_list(:user, 2)
+
+ described_class.add_users_to_group(
+ group,
+ [users.first.id, users.second],
+ described_class::MASTER
+ )
+
+ expect(group.users).to include(users.first, users.second)
+ end
+ end
+
describe 'notifications' do
describe "#after_create" do
it "sends email to user" do
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 805c15a4e5e..d85a1c1e3b2 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -15,6 +15,26 @@ describe ProjectMember, models: true do
it { is_expected.to include_module(Gitlab::ShellAdapter) }
end
+ describe '.access_level_roles' do
+ it 'returns Gitlab::Access.options' do
+ expect(described_class.access_level_roles).to eq(Gitlab::Access.options)
+ end
+ end
+
+ describe '.add_user' do
+ context 'when called with the project owner' do
+ it 'adds the user as a member' do
+ project = create(:empty_project)
+
+ expect(project.users).not_to include(project.owner)
+
+ described_class.add_user(project, project.owner, :master, current_user: project.owner)
+
+ expect(project.users.reload).to include(project.owner)
+ end
+ end
+ end
+
describe '#real_source_type' do
subject { create(:project_member).real_source_type }
@@ -50,7 +70,7 @@ describe ProjectMember, models: true do
end
end
- describe :import_team do
+ describe '.import_team' do
before do
@project_1 = create :project
@project_2 = create :project
@@ -81,25 +101,21 @@ describe ProjectMember, models: true do
end
describe '.add_users_to_projects' do
- before do
- @project_1 = create :project
- @project_2 = create :project
+ it 'adds the given users to the given projects' do
+ projects = create_list(:empty_project, 2)
+ users = create_list(:user, 2)
- @user_1 = create :user
- @user_2 = create :user
-
- ProjectMember.add_users_to_projects(
- [@project_1.id, @project_2.id],
- [@user_1.id, @user_2.id],
- ProjectMember::MASTER
- )
- end
+ described_class.add_users_to_projects(
+ [projects.first.id, projects.second],
+ [users.first.id, users.second],
+ described_class::MASTER)
- it { expect(@project_1.users).to include(@user_1) }
- it { expect(@project_1.users).to include(@user_2) }
+ expect(projects.first.users).to include(users.first)
+ expect(projects.first.users).to include(users.second)
- it { expect(@project_2.users).to include(@user_1) }
- it { expect(@project_2.users).to include(@user_2) }
+ expect(projects.second.users).to include(users.first)
+ expect(projects.second.users).to include(users.second)
+ end
end
describe '.truncate_teams' do
diff --git a/spec/models/merge_request_diff_spec.rb b/spec/models/merge_request_diff_spec.rb
index 530a7def553..96f1f60dbc0 100644
--- a/spec/models/merge_request_diff_spec.rb
+++ b/spec/models/merge_request_diff_spec.rb
@@ -44,6 +44,16 @@ describe MergeRequestDiff, models: true do
end
end
+ context 'when the raw diffs have invalid content' do
+ before { mr_diff.update_attributes(st_diffs: ["--broken-diff"]) }
+
+ it 'returns an empty DiffCollection' do
+ expect(mr_diff.raw_diffs.to_a).to be_empty
+ expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
+ expect(mr_diff.raw_diffs).to be_empty
+ end
+ end
+
context 'when the raw diffs exist' do
it 'returns the diffs' do
expect(mr_diff.raw_diffs).to be_a(Gitlab::Git::DiffCollection)
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 12df6adde44..38b6da50168 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -86,6 +86,30 @@ describe MergeRequest, models: true do
end
end
+ describe '#cache_merge_request_closes_issues!' do
+ before do
+ subject.project.team << [subject.author, :developer]
+ subject.target_branch = subject.project.default_branch
+ end
+
+ it 'caches closed issues' do
+ issue = create :issue, project: subject.project
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues! }.to change(subject.merge_requests_closing_issues, :count).by(1)
+ end
+
+ it 'does not cache issues from external trackers' do
+ subject.project.update_attribute(:has_external_issue_tracker, true)
+ issue = ExternalIssue.new('JIRA-123', subject.project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues! }.not_to change(subject.merge_requests_closing_issues, :count)
+ end
+ end
+
describe '#source_branch_sha' do
let(:last_branch_commit) { subject.source_project.repository.commit(subject.source_branch) }
@@ -287,6 +311,46 @@ describe MergeRequest, models: true do
end
end
+ describe "#wipless_title" do
+ ['WIP ', 'WIP:', 'WIP: ', '[WIP]', '[WIP] ', ' [WIP] WIP [WIP] WIP: WIP '].each do |wip_prefix|
+ it "removes the '#{wip_prefix}' prefix" do
+ wipless_title = subject.title
+ subject.title = "#{wip_prefix}#{subject.title}"
+
+ expect(subject.wipless_title).to eq wipless_title
+ end
+
+ it "is satisfies the #work_in_progress? method" do
+ subject.title = "#{wip_prefix}#{subject.title}"
+ subject.title = subject.wipless_title
+
+ expect(subject.work_in_progress?).to eq false
+ end
+ end
+ end
+
+ describe "#wip_title" do
+ it "adds the WIP: prefix to the title" do
+ wip_title = "WIP: #{subject.title}"
+
+ expect(subject.wip_title).to eq wip_title
+ end
+
+ it "does not add the WIP: prefix multiple times" do
+ wip_title = "WIP: #{subject.title}"
+ subject.title = subject.wip_title
+ subject.title = subject.wip_title
+
+ expect(subject.wip_title).to eq wip_title
+ end
+
+ it "is satisfies the #work_in_progress? method" do
+ subject.title = subject.wip_title
+
+ expect(subject.work_in_progress?).to eq true
+ end
+ end
+
describe '#can_remove_source_branch?' do
let(:user) { create(:user) }
let(:user2) { create(:user) }
@@ -328,6 +392,42 @@ describe MergeRequest, models: true do
end
end
+ describe '#merge_commit_message' do
+ it 'includes merge information as the title' do
+ request = build(:merge_request, source_branch: 'source', target_branch: 'target')
+
+ expect(request.merge_commit_message)
+ .to match("Merge branch 'source' into 'target'\n\n")
+ end
+
+ it 'includes its title in the body' do
+ request = build(:merge_request, title: 'Remove all technical debt')
+
+ expect(request.merge_commit_message)
+ .to match("Remove all technical debt\n\n")
+ end
+
+ it 'includes its description in the body' do
+ request = build(:merge_request, description: 'By removing all code')
+
+ expect(request.merge_commit_message)
+ .to match("By removing all code\n\n")
+ end
+
+ it 'includes its reference in the body' do
+ request = build_stubbed(:merge_request)
+
+ expect(request.merge_commit_message)
+ .to match("See merge request #{request.to_reference}")
+ end
+
+ it 'excludes multiple linebreak runs when description is blank' do
+ request = build(:merge_request, title: 'Title', description: nil)
+
+ expect(request.merge_commit_message).not_to match("Title\n\n\n\n")
+ end
+ end
+
describe "#reset_merge_when_build_succeeds" do
let(:merge_if_green) do
create :merge_request, merge_when_build_succeeds: true, merge_user: create(:user),
@@ -446,7 +546,7 @@ describe MergeRequest, models: true do
end
it_behaves_like 'an editable mentionable' do
- subject { create(:merge_request) }
+ subject { create(:merge_request, :simple) }
let(:backref_text) { "merge request #{subject.to_reference}" }
let(:set_mentionable_text) { ->(txt){ subject.description = txt } }
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index d64d6cde2b5..33fe22dd98c 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -20,10 +20,10 @@ describe Milestone, models: true do
let(:user) { create(:user) }
describe "#title" do
- let(:milestone) { create(:milestone, title: "<b>test</b>") }
+ let(:milestone) { create(:milestone, title: "<b>foo & bar -> 2.2</b>") }
it "sanitizes title" do
- expect(milestone.title).to eq("test")
+ expect(milestone.title).to eq("foo & bar -> 2.2")
end
end
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 544920d1824..431b3e4435f 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -114,6 +114,7 @@ describe Namespace, models: true do
it "cleans the path and makes sure it's available" do
expect(Namespace.clean_path("-john+gitlab-ETC%.git@gmail.com")).to eq("johngitlab-ETC2")
+ expect(Namespace.clean_path("--%+--valid_*&%name=.git.%.atom.atom.@email.com")).to eq("valid_name")
end
end
end
diff --git a/spec/models/project_services/custom_issue_tracker_service_spec.rb b/spec/models/project_services/custom_issue_tracker_service_spec.rb
index 7020667ea58..63320931e76 100644
--- a/spec/models/project_services/custom_issue_tracker_service_spec.rb
+++ b/spec/models/project_services/custom_issue_tracker_service_spec.rb
@@ -25,5 +25,21 @@ describe CustomIssueTrackerService, models: true do
it { is_expected.not_to validate_presence_of(:issues_url) }
it { is_expected.not_to validate_presence_of(:new_issue_url) }
end
+
+ context 'title' do
+ let(:issue_tracker) { described_class.new(properties: {}) }
+
+ it 'sets a default title' do
+ issue_tracker.title = nil
+
+ expect(issue_tracker.title).to eq('Custom Issue Tracker')
+ end
+
+ it 'sets the custom title' do
+ issue_tracker.title = 'test title'
+
+ expect(issue_tracker.title).to eq('test title')
+ end
+ end
end
end
diff --git a/spec/models/project_services/hipchat_service_spec.rb b/spec/models/project_services/hipchat_service_spec.rb
index cf713684463..26dd95bdfec 100644
--- a/spec/models/project_services/hipchat_service_spec.rb
+++ b/spec/models/project_services/hipchat_service_spec.rb
@@ -283,7 +283,7 @@ describe HipchatService, models: true do
context 'build events' do
let(:pipeline) { create(:ci_empty_pipeline) }
let(:build) { create(:ci_build, pipeline: pipeline) }
- let(:data) { Gitlab::DataBuilder::Build.build(build) }
+ let(:data) { Gitlab::DataBuilder::Build.build(build.reload) }
context 'for failed' do
before { build.drop }
diff --git a/spec/models/project_services/slack_service/issue_message_spec.rb b/spec/models/project_services/slack_service/issue_message_spec.rb
index 0f8889bdf3c..98c36ec088d 100644
--- a/spec/models/project_services/slack_service/issue_message_spec.rb
+++ b/spec/models/project_services/slack_service/issue_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::IssueMessage, models: true do
{
user: {
name: 'Test User',
- username: 'Test User'
+ username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
@@ -40,7 +40,7 @@ describe SlackService::IssueMessage, models: true do
context 'open' do
it 'returns a message regarding opening of issues' do
expect(subject.pretext).to eq(
- '<somewhere.com|[project_name>] Issue opened by Test User')
+ '<somewhere.com|[project_name>] Issue opened by test.user')
expect(subject.attachments).to eq([
{
title: "#100 Issue title",
@@ -60,7 +60,7 @@ describe SlackService::IssueMessage, models: true do
it 'returns a message regarding closing of issues' do
expect(subject.pretext). to eq(
- '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by Test User')
+ '<somewhere.com|[project_name>] Issue <url|#100 Issue title> closed by test.user')
expect(subject.attachments).to be_empty
end
end
diff --git a/spec/models/project_services/slack_service/merge_message_spec.rb b/spec/models/project_services/slack_service/merge_message_spec.rb
index 224c7ceabe8..c5c052d9af1 100644
--- a/spec/models/project_services/slack_service/merge_message_spec.rb
+++ b/spec/models/project_services/slack_service/merge_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::MergeMessage, models: true do
{
user: {
name: 'Test User',
- username: 'Test User'
+ username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
@@ -31,7 +31,7 @@ describe SlackService::MergeMessage, models: true do
context 'open' do
it 'returns a message regarding opening of merge requests' do
expect(subject.pretext).to eq(
- 'Test User opened <somewhere.com/merge_requests/100|merge request !100> '\
+ 'test.user opened <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
@@ -43,7 +43,7 @@ describe SlackService::MergeMessage, models: true do
end
it 'returns a message regarding closing of merge requests' do
expect(subject.pretext).to eq(
- 'Test User closed <somewhere.com/merge_requests/100|merge request !100> '\
+ 'test.user closed <somewhere.com/merge_requests/100|merge request !100> '\
'in <somewhere.com|project_name>: *Issue title*')
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/slack_service/note_message_spec.rb b/spec/models/project_services/slack_service/note_message_spec.rb
index 41b93f08050..38cfe4ad3e3 100644
--- a/spec/models/project_services/slack_service/note_message_spec.rb
+++ b/spec/models/project_services/slack_service/note_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::NoteMessage, models: true do
@args = {
user: {
name: 'Test User',
- username: 'username',
+ username: 'test.user',
avatar_url: 'http://fakeavatar'
},
project_name: 'project_name',
@@ -37,7 +37,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on commits' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("Test User commented on " \
+ expect(message.pretext).to eq("test.user commented on " \
"<url|commit 5f163b2b> in <somewhere.com|project_name>: " \
"*Added a commit message*")
expected_attachments = [
@@ -63,7 +63,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a merge request' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("Test User commented on " \
+ expect(message.pretext).to eq("test.user commented on " \
"<url|merge request !30> in <somewhere.com|project_name>: " \
"*merge request title*")
expected_attachments = [
@@ -90,7 +90,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on an issue' do
message = SlackService::NoteMessage.new(@args)
expect(message.pretext).to eq(
- "Test User commented on " \
+ "test.user commented on " \
"<url|issue #20> in <somewhere.com|project_name>: " \
"*issue title*")
expected_attachments = [
@@ -115,7 +115,7 @@ describe SlackService::NoteMessage, models: true do
it 'returns a message regarding notes on a project snippet' do
message = SlackService::NoteMessage.new(@args)
- expect(message.pretext).to eq("Test User commented on " \
+ expect(message.pretext).to eq("test.user commented on " \
"<url|snippet #5> in <somewhere.com|project_name>: " \
"*snippet title*")
expected_attachments = [
diff --git a/spec/models/project_services/slack_service/push_message_spec.rb b/spec/models/project_services/slack_service/push_message_spec.rb
index cda9ee670b0..17cd05e24f1 100644
--- a/spec/models/project_services/slack_service/push_message_spec.rb
+++ b/spec/models/project_services/slack_service/push_message_spec.rb
@@ -9,7 +9,7 @@ describe SlackService::PushMessage, models: true do
before: 'before',
project_name: 'project_name',
ref: 'refs/heads/master',
- user_name: 'user_name',
+ user_name: 'test.user',
project_url: 'url'
}
end
@@ -26,7 +26,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding pushes' do
expect(subject.pretext).to eq(
- 'user_name pushed to branch <url/commits/master|master> of '\
+ 'test.user pushed to branch <url/commits/master|master> of '\
'<url|project_name> (<url/compare/before...after|Compare changes>)'
)
expect(subject.attachments).to eq([
@@ -46,13 +46,13 @@ describe SlackService::PushMessage, models: true do
before: Gitlab::Git::BLANK_SHA,
project_name: 'project_name',
ref: 'refs/tags/new_tag',
- user_name: 'user_name',
+ user_name: 'test.user',
project_url: 'url'
}
end
it 'returns a message regarding pushes' do
- expect(subject.pretext).to eq('user_name pushed new tag ' \
+ expect(subject.pretext).to eq('test.user pushed new tag ' \
'<url/commits/new_tag|new_tag> to ' \
'<url|project_name>')
expect(subject.attachments).to be_empty
@@ -66,7 +66,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding a new branch' do
expect(subject.pretext).to eq(
- 'user_name pushed new branch <url/commits/master|master> to '\
+ 'test.user pushed new branch <url/commits/master|master> to '\
'<url|project_name>'
)
expect(subject.attachments).to be_empty
@@ -80,7 +80,7 @@ describe SlackService::PushMessage, models: true do
it 'returns a message regarding a removed branch' do
expect(subject.pretext).to eq(
- 'user_name removed branch master from <url|project_name>'
+ 'test.user removed branch master from <url|project_name>'
)
expect(subject.attachments).to be_empty
end
diff --git a/spec/models/project_services/slack_service/wiki_page_message_spec.rb b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
index 13aea0b0600..093911598b0 100644
--- a/spec/models/project_services/slack_service/wiki_page_message_spec.rb
+++ b/spec/models/project_services/slack_service/wiki_page_message_spec.rb
@@ -7,7 +7,7 @@ describe SlackService::WikiPageMessage, models: true do
{
user: {
name: 'Test User',
- username: 'Test User'
+ username: 'test.user'
},
project_name: 'project_name',
project_url: 'somewhere.com',
@@ -25,7 +25,7 @@ describe SlackService::WikiPageMessage, models: true do
it 'returns a message that a new wiki page was created' do
expect(subject.pretext).to eq(
- 'Test User created <url|wiki page> in <somewhere.com|project_name>: '\
+ 'test.user created <url|wiki page> in <somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
@@ -35,7 +35,7 @@ describe SlackService::WikiPageMessage, models: true do
it 'returns a message that a wiki page was updated' do
expect(subject.pretext).to eq(
- 'Test User edited <url|wiki page> in <somewhere.com|project_name>: '\
+ 'test.user edited <url|wiki page> in <somewhere.com|project_name>: '\
'*Wiki page title*')
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 83f61f0af0a..dae546a0cdc 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -68,7 +68,7 @@ describe Project, models: true do
it { is_expected.to have_many(:forks).through(:forked_project_links) }
describe '#members & #requesters' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, :public) }
let(:requester) { create(:user) }
let(:developer) { create(:user) }
before do
@@ -308,7 +308,8 @@ describe Project, models: true do
end
describe 'last_activity methods' do
- let(:timestamp) { Time.now - 2.hours }
+ let(:timestamp) { 2.hours.ago }
+ # last_activity_at gets set to created_at upon creation
let(:project) { create(:project, created_at: timestamp, updated_at: timestamp) }
describe 'last_activity' do
@@ -321,9 +322,9 @@ describe Project, models: true do
describe 'last_activity_date' do
it 'returns the creation date of the project\'s last event if present' do
- expect_any_instance_of(Event).to receive(:try_obtain_lease).and_return(true)
new_event = create(:event, project: project, created_at: Time.now)
+ project.reload
expect(project.last_activity_at.to_i).to eq(new_event.created_at.to_i)
end
@@ -520,7 +521,7 @@ describe Project, models: true do
end
describe '#cache_has_external_issue_tracker' do
- let(:project) { create(:project) }
+ let(:project) { create(:project, has_external_issue_tracker: nil) }
it 'stores true if there is any external_issue_tracker' do
services = double(:service, external_issue_trackers: [RedmineService.new])
@@ -799,32 +800,22 @@ describe Project, models: true do
end
create(:note_on_commit, project: project2)
- end
- describe 'without an explicit start date' do
- subject { described_class.trending.to_a }
-
- it 'sorts Projects by the amount of notes in descending order' do
- expect(subject).to eq([project1, project2])
- end
+ TrendingProject.refresh!
end
- describe 'with an explicit start date' do
- let(:date) { 2.months.ago }
+ subject { described_class.trending.to_a }
- subject { described_class.trending(date).to_a }
+ it 'sorts projects by the amount of notes in descending order' do
+ expect(subject).to eq([project1, project2])
+ end
- before do
- 2.times do
- # Little fix for special issue related to Fractional Seconds support for MySQL.
- # See: https://github.com/rails/rails/pull/14359/files
- create(:note_on_commit, project: project2, created_at: date + 1)
- end
+ it 'does not take system notes into account' do
+ 10.times do
+ create(:note_on_commit, project: project2, system: true)
end
- it 'sorts Projects by the amount of notes in descending order' do
- expect(subject).to eq([project2, project1])
- end
+ expect(described_class.trending.to_a).to eq([project1, project2])
end
end
@@ -836,7 +827,7 @@ describe Project, models: true do
describe 'when a user has access to a project' do
before do
- project.team.add_user(user, Gitlab::Access::MASTER)
+ project.add_user(user, Gitlab::Access::MASTER)
end
it { is_expected.to eq([project]) }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index f979d66c88c..e0f2dadf189 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -137,7 +137,7 @@ describe ProjectTeam, models: true do
describe '#find_member' do
context 'personal project' do
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
let(:requester) { create(:user) }
before do
@@ -200,7 +200,7 @@ describe ProjectTeam, models: true do
let(:requester) { create(:user) }
context 'personal project' do
- let(:project) { create(:empty_project) }
+ let(:project) { create(:empty_project, :public) }
context 'when project is not shared with group' do
before do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index db29f4d353b..4641f297465 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -97,12 +97,20 @@ describe Repository, models: true do
end
describe '#find_commits_by_message' do
- subject { repository.find_commits_by_message('submodule').map{ |k| k.id } }
+ it 'returns commits with messages containing a given string' do
+ commit_ids = repository.find_commits_by_message('submodule').map(&:id)
- it { is_expected.to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e') }
- it { is_expected.to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9') }
- it { is_expected.to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660') }
- it { is_expected.not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e') }
+ expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ expect(commit_ids).to include('6f6d7e7ed97bb5f0054f2b1df789b39ca89b6ff9')
+ expect(commit_ids).to include('cfe32cf61b73a0d5e9f13e774abde7ff789b1660')
+ expect(commit_ids).not_to include('913c66a37b4a45b9769037c55c2d238bd0942d2e')
+ end
+
+ it 'is case insensitive' do
+ commit_ids = repository.find_commits_by_message('SUBMODULE').map(&:id)
+
+ expect(commit_ids).to include('5937ac0a7beb003549fc5fd26fc247adbce4a52e')
+ end
end
describe '#blob_at' do
@@ -320,6 +328,16 @@ describe Repository, models: true do
end
end
+ describe '#create_ref' do
+ it 'redirects the call to fetch_ref' do
+ ref, ref_path = '1', '2'
+
+ expect(repository).to receive(:fetch_ref).with(repository.path_to_repo, ref, ref_path)
+
+ repository.create_ref(ref, ref_path)
+ end
+ end
+
describe "#changelog" do
before do
repository.send(:cache).expire(:changelog)
diff --git a/spec/models/service_spec.rb b/spec/models/service_spec.rb
index 05056a4bb47..43937a54b2c 100644
--- a/spec/models/service_spec.rb
+++ b/spec/models/service_spec.rb
@@ -203,6 +203,23 @@ describe Service, models: true do
end
end
+ describe 'initialize service with no properties' do
+ let(:service) do
+ GitlabIssueTrackerService.create(
+ project: create(:project),
+ title: 'random title'
+ )
+ end
+
+ it 'does not raise error' do
+ expect { service }.not_to raise_error
+ end
+
+ it 'creates the properties' do
+ expect(service.properties).to eq({ "title" => "random title" })
+ end
+ end
+
describe "callbacks" do
let(:project) { create(:project) }
let!(:service) do
@@ -221,7 +238,7 @@ describe Service, models: true do
it "updates the has_external_issue_tracker boolean" do
expect do
service.save!
- end.to change { service.project.has_external_issue_tracker }.from(nil).to(true)
+ end.to change { service.project.has_external_issue_tracker }.from(false).to(true)
end
end
diff --git a/spec/models/snippet_spec.rb b/spec/models/snippet_spec.rb
index e6bc5296398..f62f6bacbaa 100644
--- a/spec/models/snippet_spec.rb
+++ b/spec/models/snippet_spec.rb
@@ -46,6 +46,13 @@ describe Snippet, models: true do
end
end
+ describe "#content_html_invalidated?" do
+ let(:snippet) { create(:snippet, content: "md", content_html: "html", file_name: "foo.md") }
+ it "invalidates the HTML cache of content when the filename changes" do
+ expect { snippet.file_name = "foo.rb" }.to change { snippet.content_html_invalidated? }.from(false).to(true)
+ end
+ end
+
describe '.search' do
let(:snippet) { create(:snippet) }
diff --git a/spec/models/trending_project_spec.rb b/spec/models/trending_project_spec.rb
new file mode 100644
index 00000000000..cc28c6d4004
--- /dev/null
+++ b/spec/models/trending_project_spec.rb
@@ -0,0 +1,56 @@
+require 'spec_helper'
+
+describe TrendingProject do
+ let(:user) { create(:user) }
+ let(:public_project1) { create(:empty_project, :public) }
+ let(:public_project2) { create(:empty_project, :public) }
+ let(:public_project3) { create(:empty_project, :public) }
+ let(:private_project) { create(:empty_project, :private) }
+ let(:internal_project) { create(:empty_project, :internal) }
+
+ before do
+ 3.times do
+ create(:note_on_commit, project: public_project1)
+ end
+
+ 2.times do
+ create(:note_on_commit, project: public_project2)
+ end
+
+ create(:note_on_commit, project: public_project3, created_at: 5.weeks.ago)
+ create(:note_on_commit, project: private_project)
+ create(:note_on_commit, project: internal_project)
+ end
+
+ describe '.refresh!' do
+ before do
+ described_class.refresh!
+ end
+
+ it 'populates the trending projects table' do
+ expect(described_class.count).to eq(2)
+ end
+
+ it 'removes existing rows before populating the table' do
+ described_class.refresh!
+
+ expect(described_class.count).to eq(2)
+ end
+
+ it 'stores the project IDs for every trending project' do
+ rows = described_class.order(id: :asc).all
+
+ expect(rows[0].project_id).to eq(public_project1.id)
+ expect(rows[1].project_id).to eq(public_project2.id)
+ end
+
+ it 'does not store projects that fall out of the trending time range' do
+ expect(described_class.where(project_id: public_project3).any?).to eq(false)
+ end
+
+ it 'stores only public projects' do
+ expect(described_class.where(project_id: [public_project1.id, public_project2.id]).count).to eq(2)
+ expect(described_class.where(project_id: [private_project.id, internal_project.id]).count).to eq(0)
+ end
+ end
+end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a1770d96f83..65b2896930a 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -610,6 +610,23 @@ describe User, models: true do
end
end
+ describe '.find_by_ssh_key_id' do
+ context 'using an existing SSH key ID' do
+ let(:user) { create(:user) }
+ let(:key) { create(:key, user: user) }
+
+ it 'returns the corresponding User' do
+ expect(described_class.find_by_ssh_key_id(key.id)).to eq(user)
+ end
+ end
+
+ context 'using an invalid SSH key ID' do
+ it 'returns nil' do
+ expect(described_class.find_by_ssh_key_id(-1)).to be_nil
+ end
+ end
+ end
+
describe '.by_login' do
let(:username) { 'John' }
let!(:user) { create(:user, username: username) }
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index a7a06744428..43c8d884a47 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -1,20 +1,68 @@
require 'spec_helper'
describe ProjectPolicy, models: true do
- let(:project) { create(:empty_project, :public) }
let(:guest) { create(:user) }
let(:reporter) { create(:user) }
let(:dev) { create(:user) }
let(:master) { create(:user) }
let(:owner) { create(:user) }
- let(:admin) { create(:admin) }
+ let(:project) { create(:empty_project, :public, namespace: owner.namespace) }
- let(:users_ordered_by_permissions) do
- [nil, guest, reporter, dev, master, owner, admin]
+ let(:guest_permissions) do
+ [
+ :read_project, :read_board, :read_list, :read_wiki, :read_issue, :read_label,
+ :read_milestone, :read_project_snippet, :read_project_member,
+ :read_merge_request, :read_note, :create_project, :create_issue, :create_note,
+ :upload_file
+ ]
end
- let(:users_permissions) do
- users_ordered_by_permissions.map { |u| Ability.allowed(u, project).size }
+ let(:reporter_permissions) do
+ [
+ :download_code, :fork_project, :create_project_snippet, :update_issue,
+ :admin_issue, :admin_label, :admin_list, :read_commit_status, :read_build,
+ :read_container_image, :read_pipeline, :read_environment, :read_deployment
+ ]
+ end
+
+ let(:team_member_reporter_permissions) do
+ [
+ :build_download_code, :build_read_container_image
+ ]
+ end
+
+ let(:developer_permissions) do
+ [
+ :admin_merge_request, :update_merge_request, :create_commit_status,
+ :update_commit_status, :create_build, :update_build, :create_pipeline,
+ :update_pipeline, :create_merge_request, :create_wiki, :push_code,
+ :resolve_note, :create_container_image, :update_container_image,
+ :create_environment, :create_deployment
+ ]
+ end
+
+ let(:master_permissions) do
+ [
+ :push_code_to_protected_branches, :update_project_snippet, :update_environment,
+ :update_deployment, :admin_milestone, :admin_project_snippet,
+ :admin_project_member, :admin_note, :admin_wiki, :admin_project,
+ :admin_commit_status, :admin_build, :admin_container_image,
+ :admin_pipeline, :admin_environment, :admin_deployment
+ ]
+ end
+
+ let(:public_permissions) do
+ [
+ :download_code, :fork_project, :read_commit_status, :read_pipeline,
+ :read_container_image, :build_download_code, :build_read_container_image
+ ]
+ end
+
+ let(:owner_permissions) do
+ [
+ :change_namespace, :change_visibility_level, :rename_project, :remove_project,
+ :archive_project, :remove_fork_project, :destroy_merge_request, :destroy_issue
+ ]
end
before do
@@ -22,16 +70,6 @@ describe ProjectPolicy, models: true do
project.team << [master, :master]
project.team << [dev, :developer]
project.team << [reporter, :reporter]
-
- group = create(:group)
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::MASTER)
- group.add_owner(owner)
- end
-
- it 'returns increasing permissions for each level' do
- expect(users_permissions).to eq(users_permissions.sort.uniq)
end
it 'does not include the read_issue permission when the issue author is not a member of the private project' do
@@ -46,4 +84,81 @@ describe ProjectPolicy, models: true do
expect(Ability.allowed?(user, :read_issue, project)).to be_falsy
end
+
+ context 'abilities for non-public projects' do
+ let(:project) { create(:empty_project, namespace: owner.namespace) }
+
+ subject { described_class.abilities(current_user, project).to_set }
+
+ context 'with no user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'guests' do
+ let(:current_user) { guest }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.not_to include(*reporter_permissions)
+ is_expected.not_to include(*team_member_reporter_permissions)
+ is_expected.not_to include(*developer_permissions)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.not_to include(*developer_permissions)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'developer' do
+ let(:current_user) { dev }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.not_to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'master' do
+ let(:current_user) { master }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.to include(*master_permissions)
+ is_expected.not_to include(*owner_permissions)
+ end
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it do
+ is_expected.to include(*guest_permissions)
+ is_expected.to include(*reporter_permissions)
+ is_expected.not_to include(*team_member_reporter_permissions)
+ is_expected.to include(*developer_permissions)
+ is_expected.to include(*master_permissions)
+ is_expected.to include(*owner_permissions)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/access_requests_spec.rb b/spec/requests/api/access_requests_spec.rb
index d78494b76fa..b467890a403 100644
--- a/spec/requests/api/access_requests_spec.rb
+++ b/spec/requests/api/access_requests_spec.rb
@@ -64,12 +64,12 @@ describe API::AccessRequests, api: true do
context 'when authenticated as a member' do
%i[developer master].each do |type|
context "as a #{type}" do
- it 'returns 400' do
+ it 'returns 403' do
expect do
user = public_send(type)
post api("/#{source_type.pluralize}/#{source.id}/access_requests", user)
- expect(response).to have_http_status(400)
+ expect(response).to have_http_status(403)
end.not_to change { source.requesters.count }
end
end
@@ -87,6 +87,20 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as a stranger' do
+ context "when access request is disabled for the #{source_type}" do
+ before do
+ source.update(request_access_enabled: false)
+ end
+
+ it 'returns 403' do
+ expect do
+ post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
+
+ expect(response).to have_http_status(403)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
it 'returns 201' do
expect do
post api("/#{source_type.pluralize}/#{source.id}/access_requests", stranger)
@@ -181,7 +195,7 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as the access requester' do
- it 'returns 200' do
+ it 'deletes the access requester' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", access_requester)
@@ -191,7 +205,7 @@ describe API::AccessRequests, api: true do
end
context 'when authenticated as a master/owner' do
- it 'returns 200' do
+ it 'deletes the access requester' do
expect do
delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{access_requester.id}", master)
@@ -199,6 +213,16 @@ describe API::AccessRequests, api: true do
end.to change { source.requesters.count }.by(-1)
end
+ context 'user_id matches a member, not an access requester' do
+ it 'returns 404' do
+ expect do
+ delete api("/#{source_type.pluralize}/#{source.id}/access_requests/#{developer.id}", master)
+
+ expect(response).to have_http_status(404)
+ end.not_to change { source.requesters.count }
+ end
+ end
+
context 'user_id does not match an existing access requester' do
it 'returns 404' do
expect do
diff --git a/spec/requests/api/api_helpers_spec.rb b/spec/requests/api/api_helpers_spec.rb
index e66faeed705..0f41f8dc7f1 100644
--- a/spec/requests/api/api_helpers_spec.rb
+++ b/spec/requests/api/api_helpers_spec.rb
@@ -10,7 +10,8 @@ describe API::Helpers, api: true do
let(:key) { create(:key, user: user) }
let(:params) { {} }
- let(:env) { {} }
+ let(:env) { { 'REQUEST_METHOD' => 'GET' } }
+ let(:request) { Rack::Request.new(env) }
def set_env(token_usr, identifier)
clear_env
@@ -52,17 +53,43 @@ describe API::Helpers, api: true do
describe ".current_user" do
subject { current_user }
- describe "when authenticating via Warden" do
+ describe "Warden authentication" do
before { doorkeeper_guard_returns false }
- context "fails" do
- it { is_expected.to be_nil }
+ context "with invalid credentials" do
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to be_nil }
+ end
end
- context "succeeds" do
+ context "with valid credentials" do
before { warden_authenticate_returns user }
- it { is_expected.to eq(user) }
+ context "GET request" do
+ before { env['REQUEST_METHOD'] = 'GET' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "HEAD request" do
+ before { env['REQUEST_METHOD'] = 'HEAD' }
+ it { is_expected.to eq(user) }
+ end
+
+ context "PUT request" do
+ before { env['REQUEST_METHOD'] = 'PUT' }
+ it { is_expected.to be_nil }
+ end
+
+ context "POST request" do
+ before { env['REQUEST_METHOD'] = 'POST' }
+ it { is_expected.to be_nil }
+ end
+
+ context "DELETE request" do
+ before { env['REQUEST_METHOD'] = 'DELETE' }
+ it { is_expected.to be_nil }
+ end
end
end
diff --git a/spec/requests/api/boards_spec.rb b/spec/requests/api/boards_spec.rb
new file mode 100644
index 00000000000..f4b04445c6c
--- /dev/null
+++ b/spec/requests/api/boards_spec.rb
@@ -0,0 +1,192 @@
+require 'spec_helper'
+
+describe API::API, api: true do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:user2) { create(:user) }
+ let(:non_member) { create(:user) }
+ let(:guest) { create(:user) }
+ let(:admin) { create(:user, :admin) }
+ let!(:project) { create(:project, :public, creator_id: user.id, namespace: user.namespace ) }
+
+ let!(:dev_label) do
+ create(:label, title: 'Development', color: '#FFAABB', project: project)
+ end
+
+ let!(:test_label) do
+ create(:label, title: 'Testing', color: '#FFAACC', project: project)
+ end
+
+ let!(:ux_label) do
+ create(:label, title: 'UX', color: '#FF0000', project: project)
+ end
+
+ let!(:dev_list) do
+ create(:list, label: dev_label, position: 1)
+ end
+
+ let!(:test_list) do
+ create(:list, label: test_label, position: 2)
+ end
+
+ let!(:board) do
+ create(:board, project: project, lists: [dev_list, test_list])
+ end
+
+ before do
+ project.team << [user, :reporter]
+ project.team << [guest, :guest]
+ end
+
+ describe "GET /projects/:id/boards" do
+ let(:base_url) { "/projects/#{project.id}/boards" }
+
+ context "when unauthenticated" do
+ it "returns authentication error" do
+ get api(base_url)
+
+ expect(response).to have_http_status(401)
+ end
+ end
+
+ context "when authenticated" do
+ it "returns the project issue board" do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ expect(json_response.first['id']).to eq(board.id)
+ expect(json_response.first['lists']).to be_an Array
+ expect(json_response.first['lists'].length).to eq(2)
+ expect(json_response.first['lists'].last).to have_key('position')
+ end
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns issue board lists' do
+ get api(base_url, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(2)
+ expect(json_response.first['label']['name']).to eq(dev_label.title)
+ end
+
+ it 'returns 404 if board not found' do
+ get api("/projects/#{project.id}/boards/22343/lists", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "GET /projects/:id/boards/:board_id/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'returns a list' do
+ get api("#{base_url}/#{dev_list.id}", user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['id']).to eq(dev_list.id)
+ expect(json_response['label']['name']).to eq(dev_label.title)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it 'returns 404 if list not found' do
+ get api("#{base_url}/5324", user)
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ describe "POST /projects/:id/board/lists" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it 'creates a new issue board list' do
+ post api(base_url, user),
+ label_id: ux_label.id
+
+ expect(response).to have_http_status(201)
+ expect(json_response['label']['name']).to eq(ux_label.title)
+ expect(json_response['position']).to eq(3)
+ end
+
+ it 'returns 400 when creating a new list if label_id is invalid' do
+ post api(base_url, user),
+ label_id: 23423
+
+ expect(response).to have_http_status(400)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put api("#{base_url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "PUT /projects/:id/boards/:board_id/lists/:list_id to update only position" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "updates a list" do
+ put api("#{base_url}/#{test_list.id}", user),
+ position: 1
+
+ expect(response).to have_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+
+ it "returns 404 error if list id not found" do
+ put api("#{base_url}/44444", user),
+ position: 1
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "returns 403 for project members with guest role" do
+ put api("#{base_url}/#{test_list.id}", guest),
+ position: 1
+
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ describe "DELETE /projects/:id/board/lists/:list_id" do
+ let(:base_url) { "/projects/#{project.id}/boards/#{board.id}/lists" }
+
+ it "rejects a non member from deleting a list" do
+ delete api("#{base_url}/#{dev_list.id}", non_member)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "rejects a user with guest role from deleting a list" do
+ delete api("#{base_url}/#{dev_list.id}", guest)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it "returns 404 error if list id not found" do
+ delete api("#{base_url}/44444", user)
+
+ expect(response).to have_http_status(404)
+ end
+
+ context "when the user is project owner" do
+ let(:owner) { create(:user) }
+ let(:project) { create(:project, namespace: owner.namespace) }
+
+ it "deletes the list if an admin requests it" do
+ delete api("#{base_url}/#{dev_list.id}", owner)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['position']).to eq(1)
+ end
+ end
+ end
+end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index ee0b61e2ca4..95c7bbf99c9 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -30,6 +30,15 @@ describe API::API, api: true do
expect(json_response.first['commit']['id']).to eq project.commit.id
end
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
+
context 'filter project with one scope element' do
let(:query) { 'scope=pending' }
@@ -91,6 +100,15 @@ describe API::API, api: true do
expect(json_response).to be_an Array
expect(json_response.size).to eq 2
end
+
+ it 'returns pipeline data' do
+ json_build = json_response.first
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
end
context 'when pipeline has no builds' do
@@ -133,6 +151,15 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['name']).to eq('test')
end
+
+ it 'returns pipeline data' do
+ json_build = json_response
+ expect(json_build['pipeline']).not_to be_empty
+ expect(json_build['pipeline']['id']).to eq build.pipeline.id
+ expect(json_build['pipeline']['ref']).to eq build.pipeline.ref
+ expect(json_build['pipeline']['sha']).to eq build.pipeline.sha
+ expect(json_build['pipeline']['status']).to eq build.pipeline.status
+ end
end
context 'unauthorized user' do
diff --git a/spec/requests/api/commits_spec.rb b/spec/requests/api/commits_spec.rb
index 10f772c5b1a..aa610557056 100644
--- a/spec/requests/api/commits_spec.rb
+++ b/spec/requests/api/commits_spec.rb
@@ -5,7 +5,7 @@ describe API::API, api: true do
include ApiHelpers
let(:user) { create(:user) }
let(:user2) { create(:user) }
- let!(:project) { create(:project, creator_id: user.id) }
+ let!(:project) { create(:project, creator_id: user.id, namespace: user.namespace) }
let!(:master) { create(:project_member, :master, user: user, project: project) }
let!(:guest) { create(:project_member, :guest, user: user2, project: project) }
let!(:note) { create(:note_on_commit, author: user, project: project, commit_id: project.repository.commit.id, note: 'a comment on a commit') }
@@ -13,7 +13,7 @@ describe API::API, api: true do
before { project.team << [user, :reporter] }
- describe "GET /projects/:id/repository/commits" do
+ describe "List repository commits" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -69,7 +69,268 @@ describe API::API, api: true do
end
end
- describe "GET /projects:id/repository/commits/:sha" do
+ describe "Create a commit with multiple files and actions" do
+ let!(:url) { "/projects/#{project.id}/repository/commits" }
+
+ it 'returns a 403 unauthorized for user without permissions' do
+ post api(url, user2)
+
+ expect(response).to have_http_status(403)
+ end
+
+ it 'returns a 400 bad request if no params are given' do
+ post api(url, user)
+
+ expect(response).to have_http_status(400)
+ end
+
+ context :create do
+ let(:message) { 'Created file' }
+ let!(:invalid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_c_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'a new file in project repo' do
+ post api(url, user), valid_c_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file exists' do
+ post api(url, user), invalid_c_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :delete do
+ let(:message) { 'Deleted file' }
+ let!(:invalid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ }
+ ]
+ }
+ end
+ let!(:valid_d_params) do
+ {
+ branch_name: 'markdown',
+ commit_message: message,
+ actions: [
+ {
+ action: 'delete',
+ file_path: 'doc/api/users.md'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_d_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_d_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :move do
+ let(:message) { 'Moved file' }
+ let!(:invalid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+ let!(:valid_m_params) do
+ {
+ branch_name: 'feature',
+ commit_message: message,
+ actions: [
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_m_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_m_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context :update do
+ let(:message) { 'Updated file' }
+ let!(:invalid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_u_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'an existing file in project repo' do
+ post api(url, user), valid_u_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'returns a 400 bad request if file does not exist' do
+ post api(url, user), invalid_u_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+
+ context "multiple operations" do
+ let(:message) { 'Multiple actions' }
+ let!(:invalid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'doc/api/projects.md'
+ },
+ {
+ action: 'move',
+ file_path: 'CHANGELOG',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'foo/bar.baz',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+ let!(:valid_mo_params) do
+ {
+ branch_name: 'master',
+ commit_message: message,
+ actions: [
+ {
+ action: 'create',
+ file_path: 'foo/bar/baz.txt',
+ content: 'puts 8'
+ },
+ {
+ action: 'delete',
+ file_path: 'Gemfile.zip'
+ },
+ {
+ action: 'move',
+ file_path: 'VERSION.txt',
+ previous_path: 'VERSION',
+ content: '6.7.0.pre'
+ },
+ {
+ action: 'update',
+ file_path: 'files/ruby/popen.rb',
+ content: 'puts 8'
+ }
+ ]
+ }
+ end
+
+ it 'are commited as one in project repo' do
+ post api(url, user), valid_mo_params
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq(message)
+ end
+
+ it 'return a 400 bad request if there are any issues' do
+ post api(url, user), invalid_mo_params
+
+ expect(response).to have_http_status(400)
+ end
+ end
+ end
+
+ describe "Get a single commit" do
context "authorized user" do
it "returns a commit by sha" do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}", user)
@@ -122,7 +383,7 @@ describe API::API, api: true do
end
end
- describe "GET /projects:id/repository/commits/:sha/diff" do
+ describe "Get the diff of a commit" do
context "authorized user" do
before { project.team << [user2, :reporter] }
@@ -149,7 +410,7 @@ describe API::API, api: true do
end
end
- describe 'GET /projects:id/repository/commits/:sha/comments' do
+ describe 'Get the comments of a commit' do
context 'authorized user' do
it 'returns merge_request comments' do
get api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user)
@@ -174,7 +435,7 @@ describe API::API, api: true do
end
end
- describe 'POST /projects:id/repository/commits/:sha/comments' do
+ describe 'Post comment to commit' do
context 'authorized user' do
it 'returns comment' do
post api("/projects/#{project.id}/repository/commits/#{project.repository.commit.id}/comments", user), note: 'My comment'
diff --git a/spec/requests/api/fork_spec.rb b/spec/requests/api/fork_spec.rb
index 34f84f78952..e38d5745d44 100644
--- a/spec/requests/api/fork_spec.rb
+++ b/spec/requests/api/fork_spec.rb
@@ -18,7 +18,7 @@ describe API::API, api: true do
end
let(:project_user2) do
- create(:project_member, :guest, user: user2, project: project)
+ create(:project_member, :reporter, user: user2, project: project)
end
describe 'POST /projects/fork/:id' do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 1f68ef1af8f..3ba257256a0 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -45,6 +45,16 @@ describe API::API, api: true do
expect(json_response.length).to eq(2)
end
end
+
+ context "when using skip_groups in request" do
+ it "returns all groups excluding skipped groups" do
+ get api("/groups", admin), skip_groups: [group2.id]
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.length).to eq(1)
+ end
+ end
end
describe "GET /groups/:id" do
diff --git a/spec/requests/api/internal_spec.rb b/spec/requests/api/internal_spec.rb
index 46e8e6f1169..f0f590b0331 100644
--- a/spec/requests/api/internal_spec.rb
+++ b/spec/requests/api/internal_spec.rb
@@ -111,7 +111,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['username']).to eq(user.username)
- expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end
@@ -131,7 +131,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response['username']).to eq("lfs+deploy-key-#{key.id}")
- expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).value)
+ expect(json_response['lfs_token']).to eq(Gitlab::LfsToken.new(key).token)
expect(json_response['repository_http_path']).to eq(project.http_url_to_repo)
end
end
diff --git a/spec/requests/api/members_spec.rb b/spec/requests/api/members_spec.rb
index 92032f09b17..d22e0595788 100644
--- a/spec/requests/api/members_spec.rb
+++ b/spec/requests/api/members_spec.rb
@@ -97,7 +97,10 @@ describe API::Members, api: true do
shared_examples 'POST /:sources/:id/members' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { post api("/#{source_type.pluralize}/#{source.id}/members", stranger) }
+ let(:route) do
+ post api("/#{source_type.pluralize}/#{source.id}/members", stranger),
+ user_id: access_requester.id, access_level: Member::MASTER
+ end
end
context 'when authenticated as a non-member or member with insufficient rights' do
@@ -105,7 +108,8 @@ describe API::Members, api: true do
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
- post api("/#{source_type.pluralize}/#{source.id}/members", user)
+ post api("/#{source_type.pluralize}/#{source.id}/members", user),
+ user_id: access_requester.id, access_level: Member::MASTER
expect(response).to have_http_status(403)
end
@@ -174,7 +178,10 @@ describe API::Members, api: true do
shared_examples 'PUT /:sources/:id/members/:user_id' do |source_type|
context "with :sources == #{source_type.pluralize}" do
it_behaves_like 'a 404 response when source is private' do
- let(:route) { put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger) }
+ let(:route) do
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", stranger),
+ access_level: Member::MASTER
+ end
end
context 'when authenticated as a non-member or member with insufficient rights' do
@@ -182,7 +189,8 @@ describe API::Members, api: true do
context "as a #{type}" do
it 'returns 403' do
user = public_send(type)
- put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user)
+ put api("/#{source_type.pluralize}/#{source.id}/members/#{developer.id}", user),
+ access_level: Member::MASTER
expect(response).to have_http_status(403)
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index a7930c59df9..b813ee967f8 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -15,7 +15,7 @@ describe API::API, api: true do
let(:milestone) { create(:milestone, title: '1.0.0', project: project) }
before do
- project.team << [user, :reporters]
+ project.team << [user, :reporter]
end
describe "GET /projects/:id/merge_requests" do
@@ -299,7 +299,7 @@ describe API::API, api: true do
let!(:unrelated_project) { create(:project, namespace: create(:user).namespace, creator_id: user2.id) }
before :each do |each|
- fork_project.team << [user2, :reporters]
+ fork_project.team << [user2, :reporter]
end
it "returns merge_request" do
diff --git a/spec/requests/api/milestones_spec.rb b/spec/requests/api/milestones_spec.rb
index b89dac01040..dd192bea432 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/requests/api/milestones_spec.rb
@@ -104,6 +104,14 @@ describe API::API, api: true do
expect(response).to have_http_status(400)
end
+
+ it 'creates a new project with reserved html characters' do
+ post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
+
+ expect(response).to have_http_status(201)
+ expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
+ expect(json_response['description']).to be_nil
+ end
end
describe 'PUT /projects/:id/milestones/:milestone_id' do
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index 765dc8a8f66..cfcdcad74cd 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -163,9 +163,10 @@ describe API::API, 'ProjectHooks', api: true do
expect(response).to have_http_status(404)
end
- it "returns a 405 error if hook id not given" do
+ it "returns a 404 error if hook id not given" do
delete api("/projects/#{project.id}/hooks", user)
- expect(response).to have_http_status(405)
+
+ expect(response).to have_http_status(404)
end
it "returns a 404 if a user attempts to delete project hooks he/she does not own" do
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 192c7d14c13..5f19638b460 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -175,6 +175,36 @@ describe API::API, api: true do
end
end
+ describe 'GET /projects/visible' do
+ let(:public_project) { create(:project, :public) }
+
+ before do
+ public_project
+ project
+ project2
+ project3
+ project4
+ end
+
+ it 'returns the projects viewable by the user' do
+ get api('/projects/visible', user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).
+ to contain_exactly(public_project.id, project.id, project2.id, project3.id)
+ end
+
+ it 'shows only public projects when the user only has access to those' do
+ get api('/projects/visible', user2)
+
+ expect(response).to have_http_status(200)
+ expect(json_response).to be_an Array
+ expect(json_response.map { |project| project['id'] }).
+ to contain_exactly(public_project.id)
+ end
+ end
+
describe 'GET /projects/starred' do
let(:public_project) { create(:project, :public) }
@@ -232,7 +262,7 @@ describe API::API, api: true do
post api('/projects', user), project
project.each_pair do |k, v|
- next if %i{ issues_enabled merge_requests_enabled wiki_enabled }.include?(k)
+ next if %i[has_external_issue_tracker issues_enabled merge_requests_enabled wiki_enabled].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
@@ -360,7 +390,7 @@ describe API::API, api: true do
post api("/projects/user/#{user.id}", admin), project
project.each_pair do |k, v|
- next if k == :path
+ next if %i[has_external_issue_tracker path].include?(k)
expect(json_response[k.to_s]).to eq(v)
end
end
@@ -761,13 +791,16 @@ describe API::API, api: true do
let(:group) { create(:group) }
it "shares project with group" do
+ expires_at = 10.days.from_now.to_date
+
expect do
- post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER
+ post api("/projects/#{project.id}/share", user), group_id: group.id, group_access: Gitlab::Access::DEVELOPER, expires_at: expires_at
end.to change { ProjectGroupLink.count }.by(1)
expect(response.status).to eq 201
- expect(json_response['group_id']).to eq group.id
- expect(json_response['group_access']).to eq Gitlab::Access::DEVELOPER
+ expect(json_response['group_id']).to eq(group.id)
+ expect(json_response['group_access']).to eq(Gitlab::Access::DEVELOPER)
+ expect(json_response['expires_at']).to eq(expires_at.to_s)
end
it "returns a 400 error when group id is not given" do
diff --git a/spec/requests/api/settings_spec.rb b/spec/requests/api/settings_spec.rb
index 54d096e8b7f..f4903d8e0be 100644
--- a/spec/requests/api/settings_spec.rb
+++ b/spec/requests/api/settings_spec.rb
@@ -14,22 +14,38 @@ describe API::API, 'Settings', api: true do
expect(json_response['default_projects_limit']).to eq(42)
expect(json_response['signin_enabled']).to be_truthy
expect(json_response['repository_storage']).to eq('default')
+ expect(json_response['koding_enabled']).to be_falsey
+ expect(json_response['koding_url']).to be_nil
end
end
describe "PUT /application/settings" do
- before do
- storages = { 'custom' => 'tmp/tests/custom_repositories' }
- allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ context "custom repository storage type set in the config" do
+ before do
+ storages = { 'custom' => 'tmp/tests/custom_repositories' }
+ allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ end
+
+ it "updates application settings" do
+ put api("/application/settings", admin),
+ default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom', koding_enabled: true, koding_url: 'http://koding.example.com'
+ expect(response).to have_http_status(200)
+ expect(json_response['default_projects_limit']).to eq(3)
+ expect(json_response['signin_enabled']).to be_falsey
+ expect(json_response['repository_storage']).to eq('custom')
+ expect(json_response['koding_enabled']).to be_truthy
+ expect(json_response['koding_url']).to eq('http://koding.example.com')
+ end
end
- it "updates application settings" do
- put api("/application/settings", admin),
- default_projects_limit: 3, signin_enabled: false, repository_storage: 'custom'
- expect(response).to have_http_status(200)
- expect(json_response['default_projects_limit']).to eq(3)
- expect(json_response['signin_enabled']).to be_falsey
- expect(json_response['repository_storage']).to eq('custom')
+ context "missing koding_url value when koding_enabled is true" do
+ it "returns a blank parameter error message" do
+ put api("/application/settings", admin), koding_enabled: true
+
+ expect(response).to have_http_status(400)
+ expect(json_response['message']).to have_key('koding_url')
+ expect(json_response['message']['koding_url']).to include "can't be blank"
+ end
end
end
end
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index ef73778efa9..b002949b41b 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -62,6 +62,7 @@ describe API::API, api: true do
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.first.keys).to include 'email'
+ expect(json_response.first.keys).to include 'organization'
expect(json_response.first.keys).to include 'identities'
expect(json_response.first.keys).to include 'can_create_project'
expect(json_response.first.keys).to include 'two_factor_enabled'
@@ -89,8 +90,9 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "returns a 404 if invalid ID" do
+ it "returns a 404 for invalid ID" do
get api("/users/1ASDF", user)
+
expect(response).to have_http_status(404)
end
end
@@ -265,6 +267,14 @@ describe API::API, api: true do
expect(user.reload.bio).to eq('new test bio')
end
+ it "updates user with organization" do
+ put api("/users/#{user.id}", admin), { organization: 'GitLab' }
+
+ expect(response).to have_http_status(200)
+ expect(json_response['organization']).to eq('GitLab')
+ expect(user.reload.organization).to eq('GitLab')
+ end
+
it 'updates user with his own email' do
put api("/users/#{user.id}", admin), email: user.email
expect(response).to have_http_status(200)
@@ -331,8 +341,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Not found')
end
- it "raises error for invalid ID" do
- expect{put api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 if invalid ID" do
+ put api("/users/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
it 'returns 400 error if user does not validate' do
@@ -484,8 +496,9 @@ describe API::API, api: true do
end.to change{ user.emails.count }.by(1)
end
- it "raises error for invalid ID" do
+ it "returns a 400 for invalid ID" do
post api("/users/999999/emails", admin)
+
expect(response).to have_http_status(400)
end
end
@@ -516,9 +529,10 @@ describe API::API, api: true do
expect(json_response.first['email']).to eq(email.email)
end
- it "raises error for invalid ID" do
+ it "returns a 404 for invalid ID" do
put api("/users/ASDF/emails", admin)
- expect(response).to have_http_status(405)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -557,8 +571,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 Email Not Found')
end
- it "raises error for invalid ID" do
- expect{delete api("/users/ASDF/emails/bar", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/ASDF/emails/bar", admin)
+
+ expect(response).to have_http_status(404)
end
end
end
@@ -591,8 +607,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it "raises error for invalid ID" do
- expect{delete api("/users/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -645,6 +663,7 @@ describe API::API, api: true do
it "returns 404 Not Found within invalid ID" do
get api("/user/keys/42", user)
+
expect(response).to have_http_status(404)
expect(json_response['message']).to eq('404 Not found')
end
@@ -660,6 +679,7 @@ describe API::API, api: true do
it "returns 404 for invalid ID" do
get api("/users/keys/ASDF", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -718,8 +738,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "raises error for invalid ID" do
- expect{delete api("/users/keys/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/keys/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -769,6 +791,7 @@ describe API::API, api: true do
it "returns 404 for invalid ID" do
get api("/users/emails/ASDF", admin)
+
expect(response).to have_http_status(404)
end
end
@@ -816,8 +839,10 @@ describe API::API, api: true do
expect(response).to have_http_status(401)
end
- it "raises error for invalid ID" do
- expect{delete api("/users/emails/ASDF", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ delete api("/users/emails/ASDF", admin)
+
+ expect(response).to have_http_status(404)
end
end
@@ -882,8 +907,10 @@ describe API::API, api: true do
expect(json_response['message']).to eq('404 User Not Found')
end
- it "raises error for invalid ID" do
- expect{put api("/users/ASDF/block", admin) }.to raise_error(ActionController::RoutingError)
+ it "returns a 404 for invalid ID" do
+ put api("/users/ASDF/block", admin)
+
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/requests/ci/api/builds_spec.rb b/spec/requests/ci/api/builds_spec.rb
index df97f1bf7b6..7b7d62feb2c 100644
--- a/spec/requests/ci/api/builds_spec.rb
+++ b/spec/requests/ci/api/builds_spec.rb
@@ -35,18 +35,24 @@ describe Ci::API::API do
end
end
- it "starts a build" do
- register_builds info: { platform: :darwin }
-
- expect(response).to have_http_status(201)
- expect(json_response['sha']).to eq(build.sha)
- expect(runner.reload.platform).to eq("darwin")
- expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
- expect(json_response["variables"]).to include(
- { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
- { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
- { "key" => "DB_NAME", "value" => "postgres", "public" => true }
- )
+ context 'when there is a pending build' do
+ it 'starts a build' do
+ register_builds info: { platform: :darwin }
+
+ expect(response).to have_http_status(201)
+ expect(json_response['sha']).to eq(build.sha)
+ expect(runner.reload.platform).to eq("darwin")
+ expect(json_response["options"]).to eq({ "image" => "ruby:2.1", "services" => ["postgres"] })
+ expect(json_response["variables"]).to include(
+ { "key" => "CI_BUILD_NAME", "value" => "spinach", "public" => true },
+ { "key" => "CI_BUILD_STAGE", "value" => "test", "public" => true },
+ { "key" => "DB_NAME", "value" => "postgres", "public" => true }
+ )
+ end
+
+ it 'updates runner info' do
+ expect { register_builds }.to change { runner.reload.contacted_at }
+ end
end
context 'when builds are finished' do
@@ -159,13 +165,18 @@ describe Ci::API::API do
end
context 'when runner is paused' do
- let(:inactive_runner) { create(:ci_runner, :inactive, token: "InactiveRunner") }
+ let(:runner) { create(:ci_runner, :inactive, token: 'InactiveRunner') }
- before do
- register_builds inactive_runner.token
+ it 'responds with 404' do
+ register_builds
+
+ expect(response).to have_http_status 404
end
- it { expect(response).to have_http_status 404 }
+ it 'does not update runner info' do
+ expect { register_builds }
+ .not_to change { runner.reload.contacted_at }
+ end
end
def register_builds(token = runner.token, **params)
diff --git a/spec/requests/git_http_spec.rb b/spec/requests/git_http_spec.rb
index 74516686921..413d06715b3 100644
--- a/spec/requests/git_http_spec.rb
+++ b/spec/requests/git_http_spec.rb
@@ -1,508 +1,516 @@
require "spec_helper"
describe 'Git HTTP requests', lib: true do
+ include GitHttpHelpers
include WorkhorseHelpers
- let(:user) { create(:user) }
- let(:project) { create(:project, path: 'project.git-project') }
-
it "gives WWW-Authenticate hints" do
clone_get('doesnt/exist.git')
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
- context "when the project doesn't exist" do
- context "when no authentication is provided" do
- it "responds with status 401 (no project existence information leak)" do
- download('doesnt/exist.git') do |response|
- expect(response).to have_http_status(401)
- end
- end
- end
+ describe "User with no identities" do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, path: 'project.git-project') }
- context "when username and password are provided" do
- context "when authentication fails" do
- it "responds with status 401" do
- download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ context "when the project doesn't exist" do
+ context "when no authentication is provided" do
+ it "responds with status 401 (no project existence information leak)" do
+ download('doesnt/exist.git') do |response|
expect(response).to have_http_status(401)
end
end
end
- context "when authentication succeeds" do
- it "responds with status 404" do
- download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(404)
+ context "when username and password are provided" do
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ expect(response).to have_http_status(401)
+ end
end
end
- end
- end
- end
-
- context "when the Wiki for a project exists" do
- it "responds with the right project" do
- wiki = ProjectWiki.new(project)
- project.update_attribute(:visibility_level, Project::PUBLIC)
- download("/#{wiki.repository.path_with_namespace}.git") do |response|
- json_body = ActiveSupport::JSON.decode(response.body)
-
- expect(response).to have_http_status(200)
- expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ context "when authentication succeeds" do
+ it "responds with status 404" do
+ download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
end
end
- end
- context "when the project exists" do
- let(:path) { "#{project.path_with_namespace}.git" }
-
- context "when the project is public" do
- before do
+ context "when the Wiki for a project exists" do
+ it "responds with the right project" do
+ wiki = ProjectWiki.new(project)
project.update_attribute(:visibility_level, Project::PUBLIC)
- end
- it "downloads get status 200" do
- download(path, {}) do |response|
+ download("/#{wiki.repository.path_with_namespace}.git") do |response|
+ json_body = ActiveSupport::JSON.decode(response.body)
+
expect(response).to have_http_status(200)
+ expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end
+ end
+
+ context "when the project exists" do
+ let(:path) { "#{project.path_with_namespace}.git" }
- it "uploads get status 401" do
- upload(path, {}) do |response|
- expect(response).to have_http_status(401)
+ context "when the project is public" do
+ before do
+ project.update_attribute(:visibility_level, Project::PUBLIC)
end
- end
- context "with correct credentials" do
- let(:env) { { user: user.username, password: user.password } }
+ it "downloads get status 200" do
+ download(path, {}) do |response|
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+ end
- it "uploads get status 403" do
- upload(path, env) do |response|
- expect(response).to have_http_status(403)
+ it "uploads get status 401" do
+ upload(path, {}) do |response|
+ expect(response).to have_http_status(401)
end
end
- context 'but git-receive-pack is disabled' do
- it "responds with status 404" do
- allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
+ context "with correct credentials" do
+ let(:env) { { user: user.username, password: user.password } }
+ it "uploads get status 403" do
upload(path, env) do |response|
expect(response).to have_http_status(403)
end
end
- end
- end
- context 'but git-upload-pack is disabled' do
- it "responds with status 404" do
- allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
+ context 'but git-receive-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
- download(path, {}) do |response|
- expect(response).to have_http_status(404)
+ upload(path, env) do |response|
+ expect(response).to have_http_status(403)
+ end
+ end
end
end
- end
-
- context 'when the request is not from gitlab-workhorse' do
- it 'raises an exception' do
- expect do
- get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
- end.to raise_error(JWT::DecodeError)
- end
- end
- end
- context "when the project is private" do
- before do
- project.update_attribute(:visibility_level, Project::PRIVATE)
- end
+ context 'but git-upload-pack is disabled' do
+ it "responds with status 404" do
+ allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
- context "when no authentication is provided" do
- it "responds with status 401 to downloads" do
- download(path, {}) do |response|
- expect(response).to have_http_status(401)
+ download(path, {}) do |response|
+ expect(response).to have_http_status(404)
+ end
end
end
- it "responds with status 401 to uploads" do
- upload(path, {}) do |response|
- expect(response).to have_http_status(401)
+ context 'when the request is not from gitlab-workhorse' do
+ it 'raises an exception' do
+ expect do
+ get("/#{project.path_with_namespace}.git/info/refs?service=git-upload-pack")
+ end.to raise_error(JWT::DecodeError)
end
end
end
- context "when username and password are provided" do
- let(:env) { { user: user.username, password: 'nope' } }
+ context "when the project is private" do
+ before do
+ project.update_attribute(:visibility_level, Project::PRIVATE)
+ end
- context "when authentication fails" do
- it "responds with status 401" do
- download(path, env) do |response|
+ context "when no authentication is provided" do
+ it "responds with status 401 to downloads" do
+ download(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
- context "when the user is IP banned" do
- it "responds with status 401" do
- expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
-
- clone_get(path, env)
-
+ it "responds with status 401 to uploads" do
+ upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
end
- context "when authentication succeeds" do
- let(:env) { { user: user.username, password: user.password } }
+ context "when username and password are provided" do
+ let(:env) { { user: user.username, password: 'nope' } }
- context "when the user has access to the project" do
- before do
- project.team << [user, :master]
+ context "when authentication fails" do
+ it "responds with status 401" do
+ download(path, env) do |response|
+ expect(response).to have_http_status(401)
+ end
end
- context "when the user is blocked" do
- it "responds with status 404" do
- user.block
- project.team << [user, :master]
+ context "when the user is IP banned" do
+ it "responds with status 401" do
+ expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
- download(path, env) do |response|
- expect(response).to have_http_status(404)
- end
+ clone_get(path, env)
+
+ expect(response).to have_http_status(401)
end
end
+ end
- context "when the user isn't blocked" do
- it "downloads get status 200" do
- expect(Rack::Attack::Allow2Ban).to receive(:reset)
+ context "when authentication succeeds" do
+ let(:env) { { user: user.username, password: user.password } }
- clone_get(path, env)
+ context "when the user has access to the project" do
+ before do
+ project.team << [user, :master]
+ end
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ context "when the user is blocked" do
+ it "responds with status 404" do
+ user.block
+ project.team << [user, :master]
+
+ download(path, env) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
end
- it "uploads get status 200" do
- upload(path, env) do |response|
+ context "when the user isn't blocked" do
+ it "downloads get status 200" do
+ expect(Rack::Attack::Allow2Ban).to receive(:reset)
+
+ clone_get(path, env)
+
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
- end
- end
- context "when an oauth token is provided" do
- before do
- application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
- @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ it "uploads get status 200" do
+ upload(path, env) do |response|
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
+ end
end
- it "downloads get status 200" do
- clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+ context "when an oauth token is provided" do
+ before do
+ application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
+ @token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
+ end
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- end
+ it "downloads get status 200" do
+ clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
- it "uploads get status 401 (no project existence information leak)" do
- push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
- expect(response).to have_http_status(401)
+ it "uploads get status 401 (no project existence information leak)" do
+ push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
+
+ expect(response).to have_http_status(401)
+ end
end
- end
- context 'when user has 2FA enabled' do
- let(:user) { create(:user, :two_factor) }
- let(:access_token) { create(:personal_access_token, user: user) }
+ context 'when user has 2FA enabled' do
+ let(:user) { create(:user, :two_factor) }
+ let(:access_token) { create(:personal_access_token, user: user) }
- before do
- project.team << [user, :master]
- end
+ before do
+ project.team << [user, :master]
+ end
- context 'when username and password are provided' do
- it 'rejects the clone attempt' do
- download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(401)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ context 'when username and password are provided' do
+ it 'rejects the clone attempt' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
end
- end
- it 'rejects the push attempt' do
- upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(401)
- expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ it 'rejects the push attempt' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(401)
+ expect(response.body).to include('You have 2FA enabled, please use a personal access token for Git over HTTP')
+ end
end
end
- end
- context 'when username and personal access token are provided' do
- it 'allows clones' do
- download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
- expect(response).to have_http_status(200)
+ context 'when username and personal access token are provided' do
+ it 'allows clones' do
+ download("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
end
- end
- it 'allows pushes' do
- upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
- expect(response).to have_http_status(200)
+ it 'allows pushes' do
+ upload("#{project.path_with_namespace}.git", user: user.username, password: access_token.token) do |response|
+ expect(response).to have_http_status(200)
+ end
end
end
end
- end
- context "when blank password attempts follow a valid login" do
- def attempt_login(include_password)
- password = include_password ? user.password : ""
- clone_get path, user: user.username, password: password
- response.status
- end
+ context "when blank password attempts follow a valid login" do
+ def attempt_login(include_password)
+ password = include_password ? user.password : ""
+ clone_get path, user: user.username, password: password
+ response.status
+ end
- it "repeated attempts followed by successful attempt" do
- options = Gitlab.config.rack_attack.git_basic_auth
- maxretry = options[:maxretry] - 1
- ip = '1.2.3.4'
+ it "repeated attempts followed by successful attempt" do
+ options = Gitlab.config.rack_attack.git_basic_auth
+ maxretry = options[:maxretry] - 1
+ ip = '1.2.3.4'
- allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
- Rack::Attack::Allow2Ban.reset(ip, options)
+ allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
+ Rack::Attack::Allow2Ban.reset(ip, options)
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
+ end
- expect(attempt_login(true)).to eq(200)
- expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
+ expect(attempt_login(true)).to eq(200)
+ expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
- maxretry.times.each do
- expect(attempt_login(false)).to eq(401)
- end
+ maxretry.times.each do
+ expect(attempt_login(false)).to eq(401)
+ end
- Rack::Attack::Allow2Ban.reset(ip, options)
+ Rack::Attack::Allow2Ban.reset(ip, options)
+ end
end
end
- end
- context "when the user doesn't have access to the project" do
- it "downloads get status 404" do
- download(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(404)
+ context "when the user doesn't have access to the project" do
+ it "downloads get status 404" do
+ download(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
end
- end
- it "uploads get status 404" do
- upload(path, user: user.username, password: user.password) do |response|
- expect(response).to have_http_status(404)
+ it "uploads get status 404" do
+ upload(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
end
end
end
end
- end
-
- context "when a gitlab ci token is provided" do
- let(:build) { create(:ci_build, :running) }
- let(:project) { build.project }
- let(:other_project) { create(:empty_project) }
-
- before do
- project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
- end
-
- context 'when build created by system is authenticated' do
- it "downloads get status 200" do
- clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
-
- expect(response).to have_http_status(200)
- expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
- end
-
- it "uploads get status 401 (no project existence information leak)" do
- push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
-
- expect(response).to have_http_status(401)
- end
-
- it "downloads from other project get status 404" do
- clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(404)
- end
- end
+ context "when a gitlab ci token is provided" do
+ let(:build) { create(:ci_build, :running) }
+ let(:project) { build.project }
+ let(:other_project) { create(:empty_project) }
- context 'and build created by' do
before do
- build.update(user: user)
- project.team << [user, :reporter]
+ project.project_feature.update_attributes(builds_access_level: ProjectFeature::ENABLED)
end
- shared_examples 'can download code only' do
- it 'downloads get status 200' do
+ context 'when build created by system is authenticated' do
+ it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(200)
expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
- it 'uploads get status 403' do
+ it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
expect(response).to have_http_status(401)
end
+
+ it "downloads from other project get status 404" do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(404)
+ end
end
- context 'administrator' do
- let(:user) { create(:admin) }
+ context 'and build created by' do
+ before do
+ build.update(user: user)
+ project.team << [user, :reporter]
+ end
- it_behaves_like 'can download code only'
+ shared_examples 'can download code only' do
+ it 'downloads get status 200' do
+ clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- it 'downloads from other project get status 403' do
- clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+ expect(response).to have_http_status(200)
+ expect(response.content_type.to_s).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
+ end
- expect(response).to have_http_status(403)
+ it 'uploads get status 403' do
+ push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(401)
+ end
end
- end
- context 'regular user' do
- let(:user) { create(:user) }
+ context 'administrator' do
+ let(:user) { create(:admin) }
- it_behaves_like 'can download code only'
+ it_behaves_like 'can download code only'
- it 'downloads from other project get status 404' do
- clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+ it 'downloads from other project get status 403' do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
- expect(response).to have_http_status(404)
+ expect(response).to have_http_status(403)
+ end
+ end
+
+ context 'regular user' do
+ let(:user) { create(:user) }
+
+ it_behaves_like 'can download code only'
+
+ it 'downloads from other project get status 404' do
+ clone_get "#{other_project.path_with_namespace}.git", user: 'gitlab-ci-token', password: build.token
+
+ expect(response).to have_http_status(404)
+ end
end
end
end
end
end
- end
- context "when the project path doesn't end in .git" do
- context "GET info/refs" do
- let(:path) { "/#{project.path_with_namespace}/info/refs" }
+ context "when the project path doesn't end in .git" do
+ context "GET info/refs" do
+ let(:path) { "/#{project.path_with_namespace}/info/refs" }
- context "when no params are added" do
- before { get path }
+ context "when no params are added" do
+ before { get path }
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
+ end
end
- end
- context "when the upload-pack service is requested" do
- let(:params) { { service: 'git-upload-pack' } }
- before { get path, params }
+ context "when the upload-pack service is requested" do
+ let(:params) { { service: 'git-upload-pack' } }
+ before { get path, params }
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
end
- end
- context "when the receive-pack service is requested" do
- let(:params) { { service: 'git-receive-pack' } }
- before { get path, params }
+ context "when the receive-pack service is requested" do
+ let(:params) { { service: 'git-receive-pack' } }
+ before { get path, params }
- it "redirects to the .git suffix version" do
- expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ it "redirects to the .git suffix version" do
+ expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
+ end
end
- end
- context "when the params are anything else" do
- let(:params) { { service: 'git-implode-pack' } }
- before { get path, params }
+ context "when the params are anything else" do
+ let(:params) { { service: 'git-implode-pack' } }
- it "redirects to the sign-in page" do
- expect(response).to redirect_to(new_user_session_path)
+ it "fails to find a route" do
+ expect { get(path, params) }.to raise_error(ActionController::RoutingError)
+ end
end
end
- end
- context "POST git-upload-pack" do
- it "fails to find a route" do
- expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ context "POST git-upload-pack" do
+ it "fails to find a route" do
+ expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
end
- end
- context "POST git-receive-pack" do
- it "failes to find a route" do
- expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ context "POST git-receive-pack" do
+ it "failes to find a route" do
+ expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
+ end
end
end
- end
- context "retrieving an info/refs file" do
- before { project.update_attribute(:visibility_level, Project::PUBLIC) }
+ context "retrieving an info/refs file" do
+ before { project.update_attribute(:visibility_level, Project::PUBLIC) }
- context "when the file exists" do
- before do
- # Provide a dummy file in its place
- allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
- allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
- Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
- end
+ context "when the file exists" do
+ before do
+ # Provide a dummy file in its place
+ allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
+ allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
+ Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
+ end
- get "/#{project.path_with_namespace}/blob/master/info/refs"
- end
+ get "/#{project.path_with_namespace}/blob/master/info/refs"
+ end
- it "returns the file" do
- expect(response).to have_http_status(200)
+ it "returns the file" do
+ expect(response).to have_http_status(200)
+ end
end
- end
- context "when the file does not exist" do
- before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
+ context "when the file does not exist" do
+ before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
- it "returns not found" do
- expect(response).to have_http_status(404)
+ it "returns not found" do
+ expect(response).to have_http_status(404)
+ end
end
end
end
- def clone_get(project, options = {})
- get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
-
- def clone_post(project, options = {})
- post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
-
- def push_get(project, options = {})
- get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
-
- def push_post(project, options = {})
- post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
- end
+ describe "User with LDAP identity" do
+ let(:user) { create(:omniauth_user, extern_uid: dn) }
+ let(:dn) { 'uid=john,ou=people,dc=example,dc=com' }
- def download(project, user: nil, password: nil, spnego_request_token: nil)
- args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+ 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)
+ end
- clone_get(*args)
- yield response
+ context "when authentication fails" do
+ context "when no authentication is provided" do
+ it "responds with status 401" do
+ download('doesnt/exist.git') do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
- clone_post(*args)
- yield response
- end
+ context "when username and invalid password are provided" do
+ it "responds with status 401" do
+ download('doesnt/exist.git', user: user.username, password: "nope") do |response|
+ expect(response).to have_http_status(401)
+ end
+ end
+ end
+ end
- def upload(project, user: nil, password: nil, spnego_request_token: nil)
- args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+ context "when authentication succeeds" do
+ context "when the project doesn't exist" do
+ it "responds with status 404" do
+ download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
- push_get(*args)
- yield response
+ context "when the project exists" do
+ let(:project) { create(:project, path: 'project.git-project') }
- push_post(*args)
- yield response
- end
+ before do
+ project.team << [user, :master]
+ end
- def auth_env(user, password, spnego_request_token)
- env = workhorse_internal_api_request_header
- if user && password
- env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
- elsif spnego_request_token
- env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+ it "responds with status 200" do
+ clone_get(path, user: user.username, password: user.password) do |response|
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
end
-
- env
end
end
diff --git a/spec/requests/jwt_controller_spec.rb b/spec/requests/jwt_controller_spec.rb
index 6b956e63004..f0ef155bd7b 100644
--- a/spec/requests/jwt_controller_spec.rb
+++ b/spec/requests/jwt_controller_spec.rb
@@ -39,7 +39,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(403) }
+ it { expect(response).to have_http_status(401) }
end
end
@@ -77,7 +77,7 @@ describe JwtController do
subject! { get '/jwt/auth', parameters, headers }
- it { expect(response).to have_http_status(403) }
+ it { expect(response).to have_http_status(401) }
end
end
diff --git a/spec/requests/lfs_http_spec.rb b/spec/requests/lfs_http_spec.rb
index 09e4e265dd1..dbdf83a0dff 100644
--- a/spec/requests/lfs_http_spec.rb
+++ b/spec/requests/lfs_http_spec.rb
@@ -257,6 +257,29 @@ describe 'Git LFS API and storage' do
it_behaves_like 'responds with a file'
end
+ describe 'when using a user key' do
+ let(:authorization) { authorize_user_key }
+
+ context 'when user allowed' do
+ let(:update_permissions) do
+ project.team << [user, :master]
+ project.lfs_objects << lfs_object
+ end
+
+ it_behaves_like 'responds with a file'
+ end
+
+ context 'when user not allowed' do
+ let(:update_permissions) do
+ project.lfs_objects << lfs_object
+ end
+
+ it 'responds with status 404' do
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
@@ -1110,7 +1133,11 @@ describe 'Git LFS API and storage' do
end
def authorize_deploy_key
- ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).generate)
+ ActionController::HttpAuthentication::Basic.encode_credentials("lfs+deploy-key-#{key.id}", Gitlab::LfsToken.new(key).token)
+ end
+
+ def authorize_user_key
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, Gitlab::LfsToken.new(user).token)
end
def fork_project(project, user, object = nil)
diff --git a/spec/routing/routing_spec.rb b/spec/routing/routing_spec.rb
index 4bc3cddd9c2..0ee1c811dfb 100644
--- a/spec/routing/routing_spec.rb
+++ b/spec/routing/routing_spec.rb
@@ -9,7 +9,9 @@ require 'spec_helper'
# user_calendar_activities GET /u/:username/calendar_activities(.:format)
describe UsersController, "routing" do
it "to #show" do
- expect(get("/u/User")).to route_to('users#show', username: 'User')
+ allow(User).to receive(:find_by).and_return(true)
+
+ expect(get("/User")).to route_to('users#show', username: 'User')
end
it "to #groups" do
@@ -264,7 +266,9 @@ describe "Groups", "routing" do
end
it "also display group#show on the short path" do
- expect(get('/1')).to route_to('namespaces#show', id: '1')
+ allow(Group).to receive(:find_by_path).and_return(true)
+
+ expect(get('/1')).to route_to('groups#show', id: '1')
end
end
diff --git a/spec/services/boards/issues/create_service_spec.rb b/spec/services/boards/issues/create_service_spec.rb
new file mode 100644
index 00000000000..33e10e79f6d
--- /dev/null
+++ b/spec/services/boards/issues/create_service_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+
+describe Boards::Issues::CreateService, services: true do
+ describe '#execute' do
+ let(:project) { create(:project_with_board) }
+ let(:board) { project.board }
+ let(:user) { create(:user) }
+ let(:label) { create(:label, project: project, name: 'in-progress') }
+ let!(:list) { create(:list, board: board, label: label, position: 0) }
+
+ subject(:service) { described_class.new(project, user, title: 'New issue') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'delegates the create proceedings to Issues::CreateService' do
+ expect_any_instance_of(Issues::CreateService).to receive(:execute).once
+
+ service.execute(list)
+ end
+
+ it 'creates a new issue' do
+ expect { service.execute(list) }.to change(project.issues, :count).by(1)
+ end
+
+ it 'adds the label of the list to the issue' do
+ issue = service.execute(list)
+
+ expect(issue.labels).to eq [label]
+ end
+ end
+end
diff --git a/spec/services/boards/issues/list_service_spec.rb b/spec/services/boards/issues/list_service_spec.rb
index cf4c5f13635..5b9f454fd2d 100644
--- a/spec/services/boards/issues/list_service_spec.rb
+++ b/spec/services/boards/issues/list_service_spec.rb
@@ -13,10 +13,10 @@ describe Boards::Issues::ListService, services: true do
let(:p2) { create(:label, title: 'P2', project: project, priority: 2) }
let(:p3) { create(:label, title: 'P3', project: project, priority: 3) }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { project.board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:done) { project.board.done_list }
let!(:opened_issue1) { create(:labeled_issue, project: project, labels: [bug]) }
let!(:opened_issue2) { create(:labeled_issue, project: project, labels: [p2]) }
@@ -30,7 +30,7 @@ describe Boards::Issues::ListService, services: true do
let!(:closed_issue1) { create(:labeled_issue, :closed, project: project, labels: [bug]) }
let!(:closed_issue2) { create(:labeled_issue, :closed, project: project, labels: [p3]) }
let!(:closed_issue3) { create(:issue, :closed, project: project) }
- let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1, development]) }
+ let!(:closed_issue4) { create(:labeled_issue, :closed, project: project, labels: [p1]) }
before do
project.team << [user, :developer]
@@ -58,15 +58,15 @@ describe Boards::Issues::ListService, services: true do
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue2, closed_issue3, closed_issue1]
+ expect(issues).to eq [closed_issue4, closed_issue2, closed_issue3, closed_issue1]
end
- it 'returns opened/closed issues that have label list applied when listing issues from a label list' do
+ it 'returns opened issues that have label list applied when listing issues from a label list' do
params = { id: list1.id }
issues = described_class.new(project, user, params).execute
- expect(issues).to eq [closed_issue4, list1_issue3, list1_issue1, list1_issue2]
+ expect(issues).to eq [list1_issue3, list1_issue1, list1_issue2]
end
end
end
diff --git a/spec/services/boards/issues/move_service_spec.rb b/spec/services/boards/issues/move_service_spec.rb
index 0122159cab8..180f1b08631 100644
--- a/spec/services/boards/issues/move_service_spec.rb
+++ b/spec/services/boards/issues/move_service_spec.rb
@@ -10,10 +10,10 @@ describe Boards::Issues::MoveService, services: true do
let(:development) { create(:label, project: project, name: 'Development') }
let(:testing) { create(:label, project: project, name: 'Testing') }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { project.board.backlog_list }
let!(:list1) { create(:list, board: board, label: development, position: 0) }
let!(:list2) { create(:list, board: board, label: testing, position: 1) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:done) { project.board.done_list }
before do
project.team << [user, :developer]
diff --git a/spec/services/boards/lists/create_service_spec.rb b/spec/services/boards/lists/create_service_spec.rb
index 90764b86b16..bff9c1fd1fe 100644
--- a/spec/services/boards/lists/create_service_spec.rb
+++ b/spec/services/boards/lists/create_service_spec.rb
@@ -17,17 +17,15 @@ describe Boards::Lists::CreateService, services: true do
end
end
- context 'when board lists has only a backlog list' do
+ context 'when board lists has backlog, and done lists' do
it 'creates a new list at beginning of the list' do
- create(:backlog_list, board: board)
-
list = service.execute
expect(list.position).to eq 0
end
end
- context 'when board lists has only labels lists' do
+ context 'when board lists has labels lists' do
it 'creates a new list at end of the lists' do
create(:list, board: board, position: 0)
create(:list, board: board, position: 1)
@@ -40,8 +38,6 @@ describe Boards::Lists::CreateService, services: true do
context 'when board lists has backlog, label and done lists' do
it 'creates a new list at end of the label lists' do
- create(:backlog_list, board: board)
- create(:done_list, board: board)
list1 = create(:list, board: board, position: 0)
list2 = service.execute
diff --git a/spec/services/boards/lists/destroy_service_spec.rb b/spec/services/boards/lists/destroy_service_spec.rb
index 6eff445feee..474c4512471 100644
--- a/spec/services/boards/lists/destroy_service_spec.rb
+++ b/spec/services/boards/lists/destroy_service_spec.rb
@@ -15,11 +15,11 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'decrements position of higher lists' do
- backlog = create(:backlog_list, board: board)
+ backlog = project.board.backlog_list
development = create(:list, board: board, position: 0)
review = create(:list, board: board, position: 1)
staging = create(:list, board: board, position: 2)
- done = create(:done_list, board: board)
+ done = project.board.done_list
described_class.new(project, user).execute(development)
@@ -31,14 +31,14 @@ describe Boards::Lists::DestroyService, services: true do
end
it 'does not remove list from board when list type is backlog' do
- list = create(:backlog_list, board: board)
+ list = project.board.backlog_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
end
it 'does not remove list from board when list type is done' do
- list = create(:done_list, board: board)
+ list = project.board.done_list
service = described_class.new(project, user)
expect { service.execute(list) }.not_to change(board.lists, :count)
diff --git a/spec/services/boards/lists/generate_service_spec.rb b/spec/services/boards/lists/generate_service_spec.rb
index 9fd39122737..4171e4d816c 100644
--- a/spec/services/boards/lists/generate_service_spec.rb
+++ b/spec/services/boards/lists/generate_service_spec.rb
@@ -10,7 +10,7 @@ describe Boards::Lists::GenerateService, services: true do
context 'when board lists is empty' do
it 'creates the default lists' do
- expect { service.execute }.to change(board.lists, :count).by(4)
+ expect { service.execute }.to change(board.lists, :count).by(2)
end
end
@@ -24,16 +24,15 @@ describe Boards::Lists::GenerateService, services: true do
context 'when project labels does not contains any list label' do
it 'creates labels' do
- expect { service.execute }.to change(project.labels, :count).by(4)
+ expect { service.execute }.to change(project.labels, :count).by(2)
end
end
context 'when project labels contains some of list label' do
it 'creates the missing labels' do
- create(:label, project: project, name: 'Development')
- create(:label, project: project, name: 'Ready')
+ create(:label, project: project, name: 'Doing')
- expect { service.execute }.to change(project.labels, :count).by(2)
+ expect { service.execute }.to change(project.labels, :count).by(1)
end
end
end
diff --git a/spec/services/boards/lists/move_service_spec.rb b/spec/services/boards/lists/move_service_spec.rb
index 3e9b7d07fc6..102ed67449d 100644
--- a/spec/services/boards/lists/move_service_spec.rb
+++ b/spec/services/boards/lists/move_service_spec.rb
@@ -6,12 +6,12 @@ describe Boards::Lists::MoveService, services: true do
let(:board) { project.board }
let(:user) { create(:user) }
- let!(:backlog) { create(:backlog_list, board: board) }
+ let!(:backlog) { project.board.backlog_list }
let!(:planning) { create(:list, board: board, position: 0) }
let!(:development) { create(:list, board: board, position: 1) }
let!(:review) { create(:list, board: board, position: 2) }
let!(:staging) { create(:list, board: board, position: 3) }
- let!(:done) { create(:done_list, board: board) }
+ let!(:done) { project.board.done_list }
context 'when list type is set to label' do
it 'keeps position of lists when new position is nil' do
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 8326e5cd313..ff113efd916 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -18,7 +18,7 @@ describe Ci::ProcessPipelineService, services: true do
all_builds.where.not(status: [:created, :skipped])
end
- def create_builds
+ def process_pipeline
described_class.new(pipeline.project, user).execute(pipeline)
end
@@ -36,26 +36,26 @@ describe Ci::ProcessPipelineService, services: true do
end
it 'processes a pipeline' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
succeed_pending
expect(builds.success.count).to eq(2)
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
succeed_pending
expect(builds.success.count).to eq(4)
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
succeed_pending
expect(builds.success.count).to eq(5)
- expect(create_builds).to be_falsey
+ expect(process_pipeline).to be_falsey
end
it 'does not process pipeline if existing stage is running' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pending.count).to eq(2)
- expect(create_builds).to be_falsey
+ expect(process_pipeline).to be_falsey
expect(builds.pending.count).to eq(2)
end
end
@@ -67,7 +67,7 @@ describe Ci::ProcessPipelineService, services: true do
end
it 'automatically triggers a next stage when build finishes' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:drop)
@@ -88,7 +88,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when builds are successful' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -113,7 +113,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when test job fails' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -138,7 +138,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when test and test_failure jobs fail' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -164,7 +164,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when deploy job fails' do
it 'properly creates builds' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -189,7 +189,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when build is canceled in the second stage' do
it 'does not schedule builds after build has been canceled' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build')
expect(builds.pluck(:status)).to contain_exactly('pending')
pipeline.builds.running_or_pending.each(&:success)
@@ -208,7 +208,7 @@ describe Ci::ProcessPipelineService, services: true do
context 'when listing manual actions' do
it 'returns only for skipped builds' do
# currently all builds are created
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(manual_actions).to be_empty
# succeed stage build
@@ -230,6 +230,69 @@ describe Ci::ProcessPipelineService, services: true do
end
end
+ context 'when there are manual/on_failure jobs in earlier stages' do
+ before do
+ builds
+ process_pipeline
+ builds.each(&:reload)
+ end
+
+ context 'when first stage has only manual jobs' do
+ let(:builds) do
+ [create_build('build', 0, 'manual'),
+ create_build('check', 1),
+ create_build('test', 2)]
+ end
+
+ it 'starts from the second stage' do
+ expect(builds.map(&:status)).to eq(%w[skipped pending created])
+ end
+ end
+
+ context 'when second stage has only manual jobs' do
+ let(:builds) do
+ [create_build('check', 0),
+ create_build('build', 1, 'manual'),
+ create_build('test', 2)]
+ end
+
+ it 'skips second stage and continues on third stage' do
+ expect(builds.map(&:status)).to eq(%w[pending created created])
+
+ builds.first.success
+ builds.each(&:reload)
+
+ expect(builds.map(&:status)).to eq(%w[success skipped pending])
+ end
+ end
+
+ context 'when second stage has only on_failure jobs' do
+ let(:builds) do
+ [create_build('check', 0),
+ create_build('build', 1, 'on_failure'),
+ create_build('test', 2)]
+ end
+
+ it 'skips second stage and continues on third stage' do
+ expect(builds.map(&:status)).to eq(%w[pending created created])
+
+ builds.first.success
+ builds.each(&:reload)
+
+ expect(builds.map(&:status)).to eq(%w[success skipped pending])
+ end
+ end
+
+ def create_build(name, stage_idx, when_value = nil)
+ create(:ci_build,
+ :created,
+ pipeline: pipeline,
+ name: name,
+ stage_idx: stage_idx,
+ when: when_value)
+ end
+ end
+
context 'when failed build in the middle stage is retried' do
context 'when failed build is the only unsuccessful build in the stage' do
before do
@@ -242,7 +305,7 @@ describe Ci::ProcessPipelineService, services: true do
end
it 'does trigger builds in the next stage' do
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pluck(:name)).to contain_exactly('build:1', 'build:2')
pipeline.builds.running_or_pending.each(&:success)
@@ -297,14 +360,14 @@ describe Ci::ProcessPipelineService, services: true do
expect(all_builds.count).to eq(2)
# Create builds will mark the created as pending
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.count).to eq(2)
expect(all_builds.count).to eq(2)
# When we builds succeed we will create a rest of pipeline from .gitlab-ci.yml
# We will have 2 succeeded, 2 pending (from stage test), total 5 (one more build from deploy)
succeed_pending
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.success.count).to eq(2)
expect(builds.pending.count).to eq(2)
expect(all_builds.count).to eq(5)
@@ -312,14 +375,14 @@ describe Ci::ProcessPipelineService, services: true do
# When we succeed the 2 pending from stage test,
# We will queue a deploy stage, no new builds will be created
succeed_pending
- expect(create_builds).to be_truthy
+ expect(process_pipeline).to be_truthy
expect(builds.pending.count).to eq(1)
expect(builds.success.count).to eq(4)
expect(all_builds.count).to eq(5)
# When we succeed last pending build, we will have a total of 5 succeeded builds, no new builds will be created
succeed_pending
- expect(create_builds).to be_falsey
+ expect(process_pipeline).to be_falsey
expect(builds.success.count).to eq(5)
expect(all_builds.count).to eq(5)
end
diff --git a/spec/services/files/update_service_spec.rb b/spec/services/files/update_service_spec.rb
index d019e50649f..d3c37c7820f 100644
--- a/spec/services/files/update_service_spec.rb
+++ b/spec/services/files/update_service_spec.rb
@@ -41,7 +41,7 @@ describe Files::UpdateService do
it "returns a hash with the :success status " do
results = subject.execute
- expect(results).to match({ status: :success })
+ expect(results[:status]).to match(:success)
end
it "updates the file with the new contents" do
@@ -69,7 +69,7 @@ describe Files::UpdateService do
it "returns a hash with the :success status " do
results = subject.execute
- expect(results).to match({ status: :success })
+ expect(results[:status]).to match(:success)
end
it "updates the file with the new contents" do
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index 22991c5bc86..8e3e12114f2 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -448,6 +448,8 @@ describe GitPushService, services: true do
let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
before do
+ # project.create_jira_service doesn't seem to invalidate the cache here
+ project.has_external_issue_tracker = true
jira_service_settings
WebMock.stub_request(:post, jira_api_transition_url)
diff --git a/spec/services/issues/create_service_spec.rb b/spec/services/issues/create_service_spec.rb
index 58569ba96c3..1050502fa19 100644
--- a/spec/services/issues/create_service_spec.rb
+++ b/spec/services/issues/create_service_spec.rb
@@ -20,16 +20,38 @@ describe Issues::CreateService, services: true do
let(:opts) do
{ title: 'Awesome issue',
description: 'please fix',
- assignee: assignee,
+ assignee_id: assignee.id,
label_ids: labels.map(&:id),
- milestone_id: milestone.id }
+ milestone_id: milestone.id,
+ due_date: Date.tomorrow }
end
- it { expect(issue).to be_valid }
- it { expect(issue.title).to eq('Awesome issue') }
- it { expect(issue.assignee).to eq assignee }
- it { expect(issue.labels).to match_array labels }
- it { expect(issue.milestone).to eq milestone }
+ it 'creates the issue with the given params' do
+ expect(issue).to be_persisted
+ expect(issue.title).to eq('Awesome issue')
+ expect(issue.assignee).to eq assignee
+ expect(issue.labels).to match_array labels
+ expect(issue.milestone).to eq milestone
+ expect(issue.due_date).to eq Date.tomorrow
+ end
+
+ context 'when current user cannot admin issues in the project' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ end
+
+ it 'filters out params that cannot be set without the :admin_issue permission' do
+ issue = described_class.new(project, guest, opts).execute
+
+ expect(issue).to be_persisted
+ expect(issue.title).to eq('Awesome issue')
+ expect(issue.assignee).to be_nil
+ expect(issue.labels).to be_empty
+ expect(issue.milestone).to be_nil
+ expect(issue.due_date).to be_nil
+ end
+ end
it 'creates a pending todo for new assignee' do
attributes = {
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index 4f5375a3583..1638a46ed51 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -32,55 +32,84 @@ describe Issues::UpdateService, services: true do
described_class.new(project, user, opts).execute(issue)
end
- context "valid params" do
- before do
- opts = {
+ context 'valid params' do
+ let(:opts) do
+ {
title: 'New title',
description: 'Also please fix',
assignee_id: user2.id,
state_event: 'close',
- label_ids: [label.id]
+ label_ids: [label.id],
+ due_date: Date.tomorrow
}
-
- perform_enqueued_jobs do
- update_issue(opts)
- end
end
- it { expect(issue).to be_valid }
- it { expect(issue.title).to eq('New title') }
- it { expect(issue.assignee).to eq(user2) }
- it { expect(issue).to be_closed }
- it { expect(issue.labels.count).to eq(1) }
- it { expect(issue.labels.first.title).to eq(label.name) }
-
- it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
- deliveries = ActionMailer::Base.deliveries
- email = deliveries.last
- recipients = deliveries.last(2).map(&:to).flatten
- expect(recipients).to include(user2.email, user3.email)
- expect(email.subject).to include(issue.title)
+ it 'updates the issue with the given params' do
+ update_issue(opts)
+
+ expect(issue).to be_valid
+ expect(issue.title).to eq 'New title'
+ expect(issue.description).to eq 'Also please fix'
+ expect(issue.assignee).to eq user2
+ expect(issue).to be_closed
+ expect(issue.labels).to match_array [label]
+ expect(issue.due_date).to eq Date.tomorrow
end
- it 'creates system note about issue reassign' do
- note = find_note('Reassigned to')
+ context 'when current user cannot admin issues in the project' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ end
- expect(note).not_to be_nil
- expect(note.note).to include "Reassigned to \@#{user2.username}"
+ it 'filters out params that cannot be set without the :admin_issue permission' do
+ described_class.new(project, guest, opts).execute(issue)
+
+ expect(issue).to be_valid
+ expect(issue.title).to eq 'New title'
+ expect(issue.description).to eq 'Also please fix'
+ expect(issue.assignee).to eq user3
+ expect(issue.labels).to be_empty
+ expect(issue.milestone).to be_nil
+ expect(issue.due_date).to be_nil
+ end
end
- it 'creates system note about issue label edit' do
- note = find_note('Added ~')
+ context 'with background jobs processed' do
+ before do
+ perform_enqueued_jobs do
+ update_issue(opts)
+ end
+ end
+
+ it 'sends email to user2 about assign of new issue and email to user3 about issue unassignment' do
+ deliveries = ActionMailer::Base.deliveries
+ email = deliveries.last
+ recipients = deliveries.last(2).map(&:to).flatten
+ expect(recipients).to include(user2.email, user3.email)
+ expect(email.subject).to include(issue.title)
+ end
- expect(note).not_to be_nil
- expect(note.note).to include "Added ~#{label.id} label"
- end
+ it 'creates system note about issue reassign' do
+ note = find_note('Reassigned to')
- it 'creates system note about title change' do
- note = find_note('Changed title:')
+ expect(note).not_to be_nil
+ expect(note.note).to include "Reassigned to \@#{user2.username}"
+ end
- expect(note).not_to be_nil
- expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ it 'creates system note about issue label edit' do
+ note = find_note('Added ~')
+
+ expect(note).not_to be_nil
+ expect(note.note).to include "Added ~#{label.id} label"
+ end
+
+ it 'creates system note about title change' do
+ note = find_note('Changed title:')
+
+ expect(note).not_to be_nil
+ expect(note.note).to eq 'Changed title: **{-Old-} title** → **{+New+} title**'
+ 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
new file mode 100644
index 00000000000..03e296259f9
--- /dev/null
+++ b/spec/services/members/approve_access_request_service_spec.rb
@@ -0,0 +1,96 @@
+require 'spec_helper'
+
+describe Members::ApproveAccessRequestService, services: true do
+ let(:user) { create(:user) }
+ let(:access_requester) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
+
+ 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)
+ 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)
+ end
+ end
+
+ shared_examples 'a service approving an access request' do
+ it 'succeeds' do
+ expect { described_class.new(source, user, params).execute }.to change { source.requesters.count }.by(-1)
+ end
+
+ it 'returns a <Source>Member' do
+ member = described_class.new(source, user, params).execute
+
+ 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(:params) { { 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, params).execute
+
+ 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)
+ end
+ let(:params) { { user_id: access_requester.id } }
+
+ context 'when current user cannot approve access request to the project' 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 approve access request to the project' do
+ before do
+ project.team << [user, :master]
+ group.add_owner(user)
+ end
+
+ it_behaves_like 'a service approving an access request' do
+ let(:source) { project }
+ end
+
+ 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/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
index 2395445e7fd..9995f3488af 100644
--- a/spec/services/members/destroy_service_spec.rb
+++ b/spec/services/members/destroy_service_spec.rb
@@ -2,70 +2,111 @@ require 'spec_helper'
describe Members::DestroyService, services: true do
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let!(:member) { create(:project_member, source: project) }
+ let(:member_user) { create(:user) }
+ let(:project) { create(:project, :public) }
+ let(:group) { create(:group, :public) }
- context 'when member is nil' do
- before do
- project.team << [user, :developer]
+ 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)
end
+ end
- it 'does not destroy the member' do
- expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ 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)
end
end
- context 'when current user cannot destroy the given member' do
- before do
- project.team << [user, :developer]
+ 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)
+ end
+
+ context 'when the given member is an access requester' do
+ before do
+ source.members.find_by(user_id: member_user).destroy
+ source.request_access(member_user)
+ end
+ let(:access_requester) { source.requesters.find_by(user_id: member_user) }
+
+ it_behaves_like 'a service raising ActiveRecord::RecordNotFound'
+
+ %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
+
+ it 'calls Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(access_requester)
+
+ described_class.new(source, user, params).execute(scope)
+ 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)
+
+ described_class.new(source, member_user, params).execute(scope)
+ end
+ end
+ end
+ end
end
+ end
+
+ context 'when no member are found' do
+ let(:params) { { user_id: 42 } }
- it 'does not destroy the member' do
- expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ 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 current user can destroy the given member' do
+ context 'when a member is found' do
before do
- project.team << [user, :master]
+ project.team << [member_user, :developer]
+ group.add_developer(member_user)
end
+ let(:params) { { user_id: member_user.id } }
- it 'destroys the member' do
- destroy_member(member, user)
+ context 'when current user cannot destroy the given member' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { project }
+ end
- expect(member).to be_destroyed
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { group }
+ end
end
- context 'when the given member is a requester' do
+ context 'when current user can destroy the given member' do
before do
- member.update_column(:requested_at, Time.now)
+ project.team << [user, :master]
+ group.add_owner(user)
end
- it 'calls Member#after_decline_request' do
- expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
-
- destroy_member(member, user)
+ it_behaves_like 'a service destroying a member' do
+ let(:source) { project }
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(member)
-
- destroy_member(member, member.user)
- end
+ it_behaves_like 'a service destroying a member' do
+ let(:source) { group }
end
- context 'when current user is the member and ' do
- it 'does not call Member#after_decline_request' do
- expect_any_instance_of(NotificationService).not_to receive(:decline_access_request).with(member)
+ context 'when given a :id' do
+ let(:params) { { id: project.members.find_by!(user_id: user.id).id } }
- destroy_member(member, member.user)
+ it 'destroys the member' do
+ expect { described_class.new(project, user, params).execute }.
+ to change { project.members.count }.by(-1)
end
end
end
end
-
- def destroy_member(member, user)
- Members::DestroyService.new(member, user).execute
- end
end
diff --git a/spec/services/members/request_access_service_spec.rb b/spec/services/members/request_access_service_spec.rb
new file mode 100644
index 00000000000..0d2d5f03199
--- /dev/null
+++ b/spec/services/members/request_access_service_spec.rb
@@ -0,0 +1,57 @@
+require 'spec_helper'
+
+describe Members::RequestAccessService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :private) }
+ let(:group) { create(:group, :private) }
+
+ 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)
+ 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)
+ end
+
+ it 'returns a <Source>Member' do
+ member = described_class.new(source, user).execute
+
+ expect(member).to be_a "#{source.class}Member".constantize
+ expect(member.requested_at).to be_present
+ end
+ end
+
+ context 'when source is nil' do
+ it_behaves_like 'a service raising Gitlab::Access::AccessDeniedError' do
+ let(:source) { nil }
+ end
+ end
+
+ context 'when current user cannot request access to the project' 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 request access to the project' do
+ before do
+ project.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ end
+
+ it_behaves_like 'a service creating a access request' do
+ let(:source) { project }
+ end
+
+ it_behaves_like 'a service creating a access request' 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 0d586e2216b..3a3f07ddcb9 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -52,12 +52,28 @@ describe MergeRequests::BuildService, services: true do
end
end
- context 'no commits in the diff' do
- let(:commits) { [] }
+ context 'same source and target branch' do
+ let(:source_branch) { 'master' }
it 'forbids the merge request from being created' do
expect(merge_request.can_be_created).to eq(false)
end
+
+ it 'adds an error message to the merge request' do
+ expect(merge_request.errors).to contain_exactly('You must select different branches')
+ end
+ end
+
+ context 'no commits in the diff' do
+ let(:commits) { [] }
+
+ it 'allows the merge request to be created' do
+ expect(merge_request.can_be_created).to eq(true)
+ end
+
+ it 'adds a WIP prefix to the merge request title' do
+ expect(merge_request.title).to eq('WIP: Feature branch')
+ end
end
context 'one commit in the diff' do
diff --git a/spec/services/merge_requests/merge_service_spec.rb b/spec/services/merge_requests/merge_service_spec.rb
index 159f6817e8d..ee53e110aee 100644
--- a/spec/services/merge_requests/merge_service_spec.rb
+++ b/spec/services/merge_requests/merge_service_spec.rb
@@ -38,6 +38,69 @@ describe MergeRequests::MergeService, services: true do
end
end
+ context 'closes related issues' do
+ let(:service) { described_class.new(project, user, commit_message: 'Awesome message') }
+
+ before do
+ allow(project).to receive(:default_branch).and_return(merge_request.target_branch)
+ end
+
+ it 'closes GitLab issue tracker issues' do
+ issue = create :issue, project: project
+ commit = double('commit', safe_message: "Fixes #{issue.to_reference}")
+ allow(merge_request).to receive(:commits).and_return([commit])
+
+ service.execute(merge_request)
+
+ expect(issue.reload.closed?).to be_truthy
+ end
+
+ context 'with JIRA integration' do
+ include JiraServiceHelper
+
+ let(:jira_tracker) { project.create_jira_service }
+
+ before do
+ project.update_attributes!(has_external_issue_tracker: true)
+ jira_service_settings
+ end
+
+ it 'closes issues on JIRA issue tracker' do
+ jira_issue = ExternalIssue.new('JIRA-123', project)
+ commit = double('commit', safe_message: "Fixes #{jira_issue.to_reference}")
+ allow(merge_request).to receive(:commits).and_return([commit])
+
+ expect_any_instance_of(JiraService).to receive(:close_issue).with(merge_request, jira_issue).once
+
+ service.execute(merge_request)
+ end
+ end
+ end
+
+ context 'closes related todos' do
+ let(:merge_request) { create(:merge_request, assignee: user, author: user) }
+ let(:project) { merge_request.project }
+ let(:service) { MergeRequests::MergeService.new(project, user, commit_message: 'Awesome message') }
+ let!(:todo) do
+ create(:todo, :assigned,
+ project: project,
+ author: user,
+ user: user,
+ target: merge_request)
+ end
+
+ before do
+ allow(service).to receive(:execute_hooks)
+
+ perform_enqueued_jobs do
+ service.execute(merge_request)
+ todo.reload
+ end
+ end
+
+ it { expect(todo).to be_done }
+ end
+
context 'remove source branch by author' do
let(:service) do
merge_request.merge_params['force_remove_source_branch'] = '1'
diff --git a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
index 520e906b21f..9a29e400654 100644
--- a/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
+++ b/spec/services/merge_requests/merge_when_build_succeeds_service_spec.rb
@@ -110,9 +110,21 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
context 'properly handles multiple stages' do
let(:ref) { mr_merge_if_green_enabled.source_branch }
- let!(:build) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'build', stage: 'build') }
- let!(:test) { create(:ci_build, :created, pipeline: pipeline, ref: ref, name: 'test', stage: 'test') }
- let(:pipeline) { create(:ci_empty_pipeline, ref: mr_merge_if_green_enabled.source_branch, project: project) }
+ let(:sha) { project.commit(ref).id }
+
+ let(:pipeline) do
+ create(:ci_empty_pipeline, ref: ref, sha: sha, project: project)
+ end
+
+ let!(:build) do
+ create(:ci_build, :created, pipeline: pipeline, ref: ref,
+ name: 'build', stage: 'build')
+ end
+
+ let!(:test) do
+ create(:ci_build, :created, pipeline: pipeline, ref: ref,
+ name: 'test', stage: 'test')
+ end
before do
# This behavior of MergeRequest: we instantiate a new object
@@ -121,14 +133,16 @@ describe MergeRequests::MergeWhenBuildSucceedsService do
end
end
- it "doesn't merge if some stages failed" do
+ it "doesn't merge if any of stages failed" do
expect(MergeWorker).not_to receive(:perform_async)
+
build.success
test.drop
end
- it 'merge when all stages succeeded' do
+ it 'merges when all stages succeeded' do
expect(MergeWorker).to receive(:perform_async)
+
build.success
test.success
end
diff --git a/spec/services/merge_requests/refresh_service_spec.rb b/spec/services/merge_requests/refresh_service_spec.rb
index a162df5fc34..59d3912018a 100644
--- a/spec/services/merge_requests/refresh_service_spec.rb
+++ b/spec/services/merge_requests/refresh_service_spec.rb
@@ -79,8 +79,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'manual merge of source branch' do
@@ -99,8 +99,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request.diffs.size).to be > 0 }
it { expect(@fork_merge_request).to be_merged }
it { expect(@fork_merge_request.notes.last.note).to include('changed to merged') }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'push to fork repo source branch' do
@@ -149,8 +149,8 @@ describe MergeRequests::RefreshService, services: true do
it { expect(@merge_request).to be_merged }
it { expect(@fork_merge_request).to be_open }
it { expect(@fork_merge_request.notes).to be_empty }
- it { expect(@build_failed_todo).to be_pending }
- it { expect(@fork_build_failed_todo).to be_pending }
+ it { expect(@build_failed_todo).to be_done }
+ it { expect(@fork_build_failed_todo).to be_done }
end
context 'push new branch that exists in a merge request' do
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 0d152534c38..d820646ebdf 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -962,6 +962,20 @@ describe NotificationService, services: true do
should_not_email(@u_lazy_participant)
end
+ it "notifies the merger when merge_when_build_succeeds is true" do
+ merge_request.merge_when_build_succeeds = true
+ notification.merge_mr(merge_request, @u_watcher)
+
+ should_email(@u_watcher)
+ end
+
+ it "does not notify the merger when merge_when_build_succeeds is false" do
+ merge_request.merge_when_build_succeeds = false
+ notification.merge_mr(merge_request, @u_watcher)
+
+ should_not_email(@u_watcher)
+ end
+
context 'participating' do
context 'by assignee' do
before do
diff --git a/spec/services/projects/destroy_service_spec.rb b/spec/services/projects/destroy_service_spec.rb
index 29341c5e57e..7dcd03496bb 100644
--- a/spec/services/projects/destroy_service_spec.rb
+++ b/spec/services/projects/destroy_service_spec.rb
@@ -5,6 +5,7 @@ describe Projects::DestroyService, services: true do
let!(:project) { create(:project, namespace: user.namespace) }
let!(:path) { project.repository.path_to_repo }
let!(:remove_path) { path.sub(/\.git\Z/, "+#{project.id}+deleted.git") }
+ let!(:async) { false } # execute or async_execute
context 'Sidekiq inline' do
before do
@@ -28,6 +29,22 @@ describe Projects::DestroyService, services: true do
it { expect(Dir.exist?(remove_path)).to be_truthy }
end
+ context 'async delete of project with private issue visibility' do
+ let!(:async) { true }
+
+ before do
+ project.project_feature.update_attribute("issues_access_level", ProjectFeature::PRIVATE)
+ # Run sidekiq immediately to check that renamed repository will be removed
+ Sidekiq::Testing.inline! { destroy_project(project, user, {}) }
+ end
+
+ it 'deletes the project' do
+ expect(Project.all).not_to include(project)
+ expect(Dir.exist?(path)).to be_falsey
+ expect(Dir.exist?(remove_path)).to be_falsey
+ end
+ end
+
context 'container registry' do
before do
stub_container_registry_config(enabled: true)
@@ -52,6 +69,10 @@ describe Projects::DestroyService, services: true do
end
def destroy_project(project, user, params)
- Projects::DestroyService.new(project, user, params).execute
+ if async
+ Projects::DestroyService.new(project, user, params).async_execute
+ else
+ Projects::DestroyService.new(project, user, params).execute
+ end
end
end
diff --git a/spec/services/projects/fork_service_spec.rb b/spec/services/projects/fork_service_spec.rb
index ef2036c78b1..64d15c0523c 100644
--- a/spec/services/projects/fork_service_spec.rb
+++ b/spec/services/projects/fork_service_spec.rb
@@ -12,12 +12,26 @@ describe Projects::ForkService, services: true do
description: 'wow such project')
@to_namespace = create(:namespace)
@to_user = create(:user, namespace: @to_namespace)
+ @from_project.add_user(@to_user, :developer)
end
context 'fork project' do
+ context 'when forker is a guest' do
+ before do
+ @guest = create(:user)
+ @from_project.add_user(@guest, :guest)
+ end
+ subject { fork_project(@from_project, @guest) }
+
+ it { is_expected.not_to be_persisted }
+ it { expect(subject.errors[:forked_from_project_id]).to eq(['is forbidden']) }
+ end
+
describe "successfully creates project in the user namespace" do
let(:to_project) { fork_project(@from_project, @to_user) }
+ it { expect(to_project).to be_persisted }
+ it { expect(to_project.errors).to be_empty }
it { expect(to_project.owner).to eq(@to_user) }
it { expect(to_project.namespace).to eq(@to_user.namespace) }
it { expect(to_project.star_count).to be_zero }
@@ -29,7 +43,9 @@ describe Projects::ForkService, services: true do
it "fails due to validation, not transaction failure" do
@existing_project = create(:project, creator_id: @to_user.id, name: @from_project.name, namespace: @to_namespace)
@to_project = fork_project(@from_project, @to_user)
- expect(@existing_project.persisted?).to be_truthy
+ expect(@existing_project).to be_persisted
+
+ expect(@to_project).not_to be_persisted
expect(@to_project.errors[:name]).to eq(['has already been taken'])
expect(@to_project.errors[:path]).to eq(['has already been taken'])
end
@@ -81,18 +97,23 @@ describe Projects::ForkService, services: true do
@group = create(:group)
@group.add_user(@group_owner, GroupMember::OWNER)
@group.add_user(@developer, GroupMember::DEVELOPER)
+ @project.add_user(@developer, :developer)
+ @project.add_user(@group_owner, :developer)
@opts = { namespace: @group }
end
context 'fork project for group' do
it 'group owner successfully forks project into the group' do
to_project = fork_project(@project, @group_owner, @opts)
+
+ expect(to_project).to be_persisted
+ expect(to_project.errors).to be_empty
expect(to_project.owner).to eq(@group)
expect(to_project.namespace).to eq(@group)
expect(to_project.name).to eq(@project.name)
expect(to_project.path).to eq(@project.path)
expect(to_project.description).to eq(@project.description)
- expect(to_project.star_count).to be_zero
+ expect(to_project.star_count).to be_zero
end
end
diff --git a/spec/services/projects/import_service_spec.rb b/spec/services/projects/import_service_spec.rb
index d5d4d7c56ef..ed1384798ab 100644
--- a/spec/services/projects/import_service_spec.rb
+++ b/spec/services/projects/import_service_spec.rb
@@ -108,6 +108,16 @@ describe Projects::ImportService, services: true do
expect(result[:status]).to eq :error
expect(result[:message]).to eq 'Github: failed to connect API'
end
+
+ it 'expires existence cache after error' do
+ allow_any_instance_of(Project).to receive(:repository_exists?).and_return(true)
+
+ expect_any_instance_of(Gitlab::Shell).to receive(:import_repository).with(project.repository_storage_path, project.path_with_namespace, project.import_url).and_raise(Gitlab::Shell::Error.new('Failed to import the repository'))
+ expect_any_instance_of(Repository).to receive(:expire_emptiness_caches).and_call_original
+ expect_any_instance_of(Repository).to receive(:expire_exists_cache).and_call_original
+
+ subject.execute
+ end
end
def stub_github_omniauth_provider
diff --git a/spec/services/slash_commands/interpret_service_spec.rb b/spec/services/slash_commands/interpret_service_spec.rb
index a616275e883..ae4d286d250 100644
--- a/spec/services/slash_commands/interpret_service_spec.rb
+++ b/spec/services/slash_commands/interpret_service_spec.rb
@@ -1,19 +1,19 @@
require 'spec_helper'
describe SlashCommands::InterpretService, services: true do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
+ let(:project) { create(:empty_project, :public) }
+ let(:developer) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:milestone) { create(:milestone, project: project, title: '9.10') }
let(:inprogress) { create(:label, project: project, title: 'In Progress') }
let(:bug) { create(:label, project: project, title: 'Bug') }
before do
- project.team << [user, :developer]
+ project.team << [developer, :developer]
end
describe '#execute' do
- let(:service) { described_class.new(project, user) }
+ let(:service) { described_class.new(project, developer) }
let(:merge_request) { create(:merge_request, source_project: project) }
shared_examples 'reopen command' do
@@ -45,13 +45,13 @@ describe SlashCommands::InterpretService, services: true do
it 'fetches assignee and populates assignee_id if content contains /assign' do
_, updates = service.execute(content, issuable)
- expect(updates).to eq(assignee_id: user.id)
+ expect(updates).to eq(assignee_id: developer.id)
end
end
shared_examples 'unassign command' do
it 'populates assignee_id: nil if content contains /unassign' do
- issuable.update(assignee_id: user.id)
+ issuable.update(assignee_id: developer.id)
_, updates = service.execute(content, issuable)
expect(updates).to eq(assignee_id: nil)
@@ -124,7 +124,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'done command' do
it 'populates todo_event: "done" if content contains /done' do
- TodoService.new.mark_todo(issuable, user)
+ TodoService.new.mark_todo(issuable, developer)
_, updates = service.execute(content, issuable)
expect(updates).to eq(todo_event: 'done')
@@ -141,7 +141,7 @@ describe SlashCommands::InterpretService, services: true do
shared_examples 'unsubscribe command' do
it 'populates subscription_event: "unsubscribe" if content contains /unsubscribe' do
- issuable.subscribe(user)
+ issuable.subscribe(developer)
_, updates = service.execute(content, issuable)
expect(updates).to eq(subscription_event: 'unsubscribe')
@@ -165,6 +165,23 @@ describe SlashCommands::InterpretService, services: true do
end
end
+ shared_examples 'wip command' do
+ it 'returns wip_event: "wip" if content contains /wip' do
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(wip_event: 'wip')
+ end
+ end
+
+ shared_examples 'unwip command' do
+ it 'returns wip_event: "unwip" if content contains /wip' do
+ issuable.update(title: issuable.wip_title)
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(wip_event: 'unwip')
+ end
+ end
+
shared_examples 'empty command' do
it 'populates {} if content contains an unsupported command' do
_, updates = service.execute(content, issuable)
@@ -209,12 +226,12 @@ describe SlashCommands::InterpretService, services: true do
end
it_behaves_like 'assign command' do
- let(:content) { "/assign @#{user.username}" }
+ let(:content) { "/assign @#{developer.username}" }
let(:issuable) { issue }
end
it_behaves_like 'assign command' do
- let(:content) { "/assign @#{user.username}" }
+ let(:content) { "/assign @#{developer.username}" }
let(:issuable) { merge_request }
end
@@ -376,9 +393,70 @@ describe SlashCommands::InterpretService, services: true do
let(:issuable) { issue }
end
+ it_behaves_like 'wip command' do
+ let(:content) { '/wip' }
+ let(:issuable) { merge_request }
+ end
+
+ it_behaves_like 'unwip command' do
+ let(:content) { '/wip' }
+ let(:issuable) { merge_request }
+ end
+
it_behaves_like 'empty command' do
let(:content) { '/remove_due_date' }
let(:issuable) { merge_request }
end
+
+ context 'when current_user cannot :admin_issue' do
+ let(:visitor) { create(:user) }
+ let(:issue) { create(:issue, project: project, author: visitor) }
+ let(:service) { described_class.new(project, visitor) }
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/assign @#{developer.username}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/unassign' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/milestone %#{milestone.title}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/remove_milestone' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { %(/label ~"#{inprogress.title}" ~#{bug.title} ~unknown) }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { %(/unlabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { %(/relabel ~"#{inprogress.title}") }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/due tomorrow' }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/remove_due_date' }
+ let(:issuable) { issue }
+ end
+ end
end
end
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 3d854a959f3..304d4e62396 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -40,6 +40,12 @@ describe SystemNoteService, services: true do
describe 'note body' do
let(:note_lines) { subject.note.split("\n").reject(&:blank?) }
+ describe 'comparison diff link line' do
+ it 'adds the comparison text' do
+ expect(note_lines[2]).to match "[Compare with previous version]"
+ end
+ end
+
context 'without existing commits' do
it 'adds a message header' do
expect(note_lines[0]).to eq "Added #{new_commits.size} commits:"
@@ -445,7 +451,7 @@ describe SystemNoteService, services: true do
end
context 'commit with cross-reference from fork' do
- let(:author2) { create(:user) }
+ let(:author2) { create(:project_member, :reporter, user: create(:user), project: project).user }
let(:forked_project) { Projects::ForkService.new(project, author2).execute }
let(:commit2) { forked_project.commit }
@@ -525,12 +531,12 @@ describe SystemNoteService, services: true do
include JiraServiceHelper
describe 'JIRA integration' do
- let(:project) { create(:project) }
+ let(:project) { create(:jira_project) }
let(:author) { create(:user) }
let(:issue) { create(:issue, project: project) }
let(:mergereq) { create(:merge_request, :simple, target_project: project, source_project: project) }
let(:jira_issue) { ExternalIssue.new("JIRA-1", project)}
- let(:jira_tracker) { project.create_jira_service if project.jira_service.nil? }
+ let(:jira_tracker) { project.jira_service }
let(:commit) { project.commit }
context 'in JIRA issue tracker' do
@@ -539,10 +545,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:post, jira_api_comment_url)
end
- after do
- jira_tracker.destroy!
- end
-
describe "new reference" do
before do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
@@ -555,7 +557,7 @@ describe SystemNoteService, services: true do
describe "existing reference" do
before do
- message = %Q{[#{author.name}|http://localhost/u/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
+ message = %Q{[#{author.name}|http://localhost/#{author.username}] mentioned this issue in [a commit of #{project.path_with_namespace}|http://localhost/#{project.path_with_namespace}/commit/#{commit.id}]:\\n'#{commit.title}'}
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: %Q({"comments":[{"body":"#{message}"}]}))
end
@@ -572,10 +574,6 @@ describe SystemNoteService, services: true do
WebMock.stub_request(:get, jira_api_comment_url).to_return(body: jira_issue_comments)
end
- after do
- jira_tracker.destroy!
- end
-
subject { described_class.cross_reference(jira_issue, issue, author) }
it { is_expected.to eq(jira_status_message) }
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 02b2b3ca101..f313bd4f249 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,7 +26,7 @@ RSpec.configure do |config|
config.verbose_retry = true
config.display_try_failure_messages = true
- config.include Devise::TestHelpers, type: :controller
+ config.include Devise::Test::ControllerHelpers, type: :controller
config.include Warden::Test::Helpers, type: :request
config.include LoginHelpers, type: :feature
config.include StubConfiguration
@@ -50,6 +50,11 @@ RSpec.configure do |config|
example.run
Rails.cache = caching_store
end
+
+ config.after(:each) do
+ FileUtils.rm_rf("tmp/tests/repositories")
+ FileUtils.mkdir_p("tmp/tests/repositories")
+ end
end
FactoryGirl::SyntaxRunner.class_eval do
diff --git a/spec/support/cycle_analytics_helpers.rb b/spec/support/cycle_analytics_helpers.rb
index e8e760a6187..62a5b46d47b 100644
--- a/spec/support/cycle_analytics_helpers.rb
+++ b/spec/support/cycle_analytics_helpers.rb
@@ -4,24 +4,28 @@ module CycleAnalyticsHelpers
create_commit("Commit for ##{issue.iid}", issue.project, user, branch_name)
end
- def create_commit(message, project, user, branch_name)
- filename = random_git_name
+ def create_commit(message, project, user, branch_name, count: 1)
oldrev = project.repository.commit(branch_name).sha
+ commit_shas = Array.new(count) do |index|
+ filename = random_git_name
- options = {
- committer: project.repository.user_to_committer(user),
- author: project.repository.user_to_committer(user),
- commit: { message: message, branch: branch_name, update_ref: true },
- file: { content: "content", path: filename, update: false }
- }
+ options = {
+ committer: project.repository.user_to_committer(user),
+ author: project.repository.user_to_committer(user),
+ commit: { message: message, branch: branch_name, update_ref: true },
+ file: { content: "content", path: filename, update: false }
+ }
+
+ commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
+ project.repository.commit(commit_sha)
- commit_sha = Gitlab::Git::Blob.commit(project.repository, options)
- project.repository.commit(commit_sha)
+ commit_sha
+ end
GitPushService.new(project,
user,
oldrev: oldrev,
- newrev: commit_sha,
+ newrev: commit_shas.last,
ref: 'refs/heads/master').execute
end
diff --git a/spec/support/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 5e3b8f2b23e..5e3b8f2b23e 100644
--- a/spec/support/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
diff --git a/spec/support/git_http_helpers.rb b/spec/support/git_http_helpers.rb
new file mode 100644
index 00000000000..46b686fce94
--- /dev/null
+++ b/spec/support/git_http_helpers.rb
@@ -0,0 +1,48 @@
+module GitHttpHelpers
+ def clone_get(project, options = {})
+ get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def clone_post(project, options = {})
+ post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_get(project, options = {})
+ get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def push_post(project, options = {})
+ post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
+ end
+
+ def download(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ clone_get(*args)
+ yield response
+
+ clone_post(*args)
+ yield response
+ end
+
+ def upload(project, user: nil, password: nil, spnego_request_token: nil)
+ args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
+
+ push_get(*args)
+ yield response
+
+ push_post(*args)
+ yield response
+ end
+
+ def auth_env(user, password, spnego_request_token)
+ env = workhorse_internal_api_request_header
+ if user && password
+ env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
+ elsif spnego_request_token
+ env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
+ end
+
+ env
+ end
+end
diff --git a/spec/support/import_export/export_file_helper.rb b/spec/support/import_export/export_file_helper.rb
index be0772d6a4a..1b0a4583f5c 100644
--- a/spec/support/import_export/export_file_helper.rb
+++ b/spec/support/import_export/export_file_helper.rb
@@ -130,4 +130,8 @@ module ExportFileHelper
(parsed_model_attributes - parent.keys - excluded_attributes).empty?
end
+
+ def file_permissions(file)
+ File.stat(file).mode & 0777
+ end
end
diff --git a/spec/support/matchers/have_issuable_counts.rb b/spec/support/matchers/have_issuable_counts.rb
new file mode 100644
index 00000000000..02605d6b70e
--- /dev/null
+++ b/spec/support/matchers/have_issuable_counts.rb
@@ -0,0 +1,21 @@
+RSpec::Matchers.define :have_issuable_counts do |opts|
+ match do |actual|
+ expected_counts = opts.map do |state, count|
+ "#{state.to_s.humanize} #{count}"
+ end
+
+ actual.within '.issues-state-filters' do
+ expected_counts.each do |expected_count|
+ expect(actual).to have_content(expected_count)
+ end
+ end
+ end
+
+ description do
+ "displays the following issuable counts: #{expected_counts.inspect}"
+ end
+
+ failure_message do
+ "expected the following issuable counts: #{expected_counts.inspect} to be displayed"
+ end
+end
diff --git a/spec/support/mentionable_shared_examples.rb b/spec/support/mentionable_shared_examples.rb
index e876d44c166..f57c82809a6 100644
--- a/spec/support/mentionable_shared_examples.rb
+++ b/spec/support/mentionable_shared_examples.rb
@@ -9,7 +9,7 @@ shared_context 'mentionable context' do
let(:author) { subject.author }
let(:mentioned_issue) { create(:issue, project: project) }
- let!(:mentioned_mr) { create(:merge_request, :simple, source_project: project) }
+ let!(:mentioned_mr) { create(:merge_request, source_project: project) }
let(:mentioned_commit) { project.commit("HEAD~1") }
let(:ext_proj) { create(:project, :public) }
@@ -100,6 +100,7 @@ shared_examples 'an editable mentionable' do
it 'creates new cross-reference notes when the mentionable text is edited' do
subject.save
+ subject.create_cross_references!
new_text = <<-MSG.strip_heredoc
These references already existed:
@@ -131,6 +132,7 @@ shared_examples 'an editable mentionable' do
end
# These two issues are new and should receive reference notes
+ # In the case of MergeRequests remember that cannot mention commits included in the MergeRequest
new_issues.each do |newref|
expect(SystemNoteService).to receive(:cross_reference).
with(newref, subject.local_reference, author)
diff --git a/spec/support/issuable_create_service_slash_commands_shared_examples.rb b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
index 5f9645ed44f..5f9645ed44f 100644
--- a/spec/support/issuable_create_service_slash_commands_shared_examples.rb
+++ b/spec/support/services/issuable_create_service_slash_commands_shared_examples.rb
diff --git a/spec/support/snippets_shared_examples.rb b/spec/support/snippets_shared_examples.rb
new file mode 100644
index 00000000000..57dfff3471f
--- /dev/null
+++ b/spec/support/snippets_shared_examples.rb
@@ -0,0 +1,18 @@
+# These shared examples expect a `snippets` array of snippets
+RSpec.shared_examples 'paginated snippets' do |remote: false|
+ it "is limited to #{Snippet.default_per_page} items per page" do
+ expect(page.all('.snippets-list-holder .snippet-row').count).to eq(Snippet.default_per_page)
+ end
+
+ context 'clicking on the link to the second page' do
+ before do
+ click_link('2')
+ wait_for_ajax if remote
+ end
+
+ it 'shows the remaining snippets' do
+ remaining_snippets_count = [snippets.size - Snippet.default_per_page, Snippet.default_per_page].min
+ expect(page).to have_selector('.snippets-list-holder .snippet-row', count: remaining_snippets_count)
+ end
+ end
+end
diff --git a/spec/tasks/gitlab/users_rake_spec.rb b/spec/tasks/gitlab/users_rake_spec.rb
new file mode 100644
index 00000000000..e6ebef82b78
--- /dev/null
+++ b/spec/tasks/gitlab/users_rake_spec.rb
@@ -0,0 +1,38 @@
+require 'spec_helper'
+require 'rake'
+
+describe 'gitlab:users namespace rake task' do
+ let(:enable_registry) { true }
+
+ before :all do
+ Rake.application.rake_require 'tasks/gitlab/task_helpers'
+ Rake.application.rake_require 'tasks/gitlab/users'
+
+ # empty task as env is already loaded
+ Rake::Task.define_task :environment
+ end
+
+ def run_rake_task(task_name)
+ Rake::Task[task_name].reenable
+ Rake.application.invoke_task task_name
+ end
+
+ describe 'clear_all_authentication_tokens' do
+ before do
+ # avoid writing task output to spec progress
+ allow($stdout).to receive :write
+ end
+
+ context 'gitlab version' do
+ it 'clears the authentication token for all users' do
+ create_list(:user, 2)
+
+ expect(User.pluck(:authentication_token)).to all(be_present)
+
+ run_rake_task('gitlab:users:clear_all_authentication_tokens')
+
+ expect(User.pluck(:authentication_token)).to all(be_nil)
+ end
+ end
+ end
+end
diff --git a/spec/views/admin/dashboard/index.html.haml_spec.rb b/spec/views/admin/dashboard/index.html.haml_spec.rb
index dae858a52f6..68d2d72876e 100644
--- a/spec/views/admin/dashboard/index.html.haml_spec.rb
+++ b/spec/views/admin/dashboard/index.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'admin/dashboard/index.html.haml' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
before do
assign(:projects, create_list(:empty_project, 1))
diff --git a/spec/views/ci/lints/show.html.haml_spec.rb b/spec/views/ci/lints/show.html.haml_spec.rb
new file mode 100644
index 00000000000..2dac5ee23c8
--- /dev/null
+++ b/spec/views/ci/lints/show.html.haml_spec.rb
@@ -0,0 +1,97 @@
+require 'spec_helper'
+
+describe 'ci/lints/show' do
+ include Devise::TestHelpers
+
+ describe 'XSS protection' do
+ let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+ before do
+ assign(:status, true)
+ assign(:builds, config_processor.builds)
+ assign(:stages, config_processor.stages)
+ assign(:jobs, config_processor.jobs)
+ end
+
+ context 'when builds attrbiutes contain HTML nodes' do
+ let(:content) do
+ {
+ rspec: {
+ script: '<h1>rspec</h1>',
+ stage: 'test'
+ }
+ }
+ end
+
+ it 'does not render HTML elements' do
+ render
+
+ expect(rendered).not_to have_css('h1', text: 'rspec')
+ end
+ end
+
+ context 'when builds attributes do not contain HTML nodes' do
+ let(:content) do
+ {
+ rspec: {
+ script: 'rspec',
+ stage: 'test'
+ }
+ }
+ end
+
+ it 'shows configuration in the table' do
+ render
+
+ expect(rendered).to have_css('td pre', text: 'rspec')
+ end
+ end
+ end
+
+ let(:content) do
+ {
+ build_template: {
+ script: './build.sh',
+ tags: ['dotnet'],
+ only: ['test@dude/repo'],
+ except: ['deploy'],
+ environment: 'testing'
+ }
+ }
+ end
+
+ let(:config_processor) { Ci::GitlabCiYamlProcessor.new(YAML.dump(content)) }
+
+ context 'when the content is valid' do
+ before do
+ assign(:status, true)
+ assign(:builds, config_processor.builds)
+ assign(:stages, config_processor.stages)
+ assign(:jobs, config_processor.jobs)
+ end
+
+ it 'shows the correct values' do
+ render
+
+ expect(rendered).to have_content('Tag list: dotnet')
+ expect(rendered).to have_content('Refs only: test@dude/repo')
+ expect(rendered).to have_content('Refs except: deploy')
+ expect(rendered).to have_content('Environment: testing')
+ expect(rendered).to have_content('When: on_success')
+ end
+ end
+
+ context 'when the content is invalid' do
+ before do
+ assign(:status, false)
+ assign(:error, 'Undefined error')
+ end
+
+ it 'shows error message' do
+ render
+
+ expect(rendered).to have_content('Status: syntax is incorrect')
+ expect(rendered).to have_content('Error: Undefined error')
+ expect(rendered).not_to have_content('Tag list:')
+ end
+ end
+end
diff --git a/spec/views/projects/builds/show.html.haml_spec.rb b/spec/views/projects/builds/show.html.haml_spec.rb
index 446ba3bfa14..da43622d3f9 100644
--- a/spec/views/projects/builds/show.html.haml_spec.rb
+++ b/spec/views/projects/builds/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/builds/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:pipeline) do
diff --git a/spec/views/projects/issues/_related_branches.html.haml_spec.rb b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
index 78af61f15a7..c8a3d02d8fd 100644
--- a/spec/views/projects/issues/_related_branches.html.haml_spec.rb
+++ b/spec/views/projects/issues/_related_branches.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/issues/_related_branches' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:branch) { project.repository.find_branch('feature') }
diff --git a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
index 21f49d396e7..86980f59cd8 100644
--- a/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/_heading.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/merge_requests/widget/_heading' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
context 'when released to an environment' do
let(:project) { merge_request.target_project }
diff --git a/spec/views/projects/merge_requests/edit.html.haml_spec.rb b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
index 31bbb150698..3650b22c389 100644
--- a/spec/views/projects/merge_requests/edit.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/edit.html.haml_spec.rb
@@ -1,18 +1,21 @@
require 'spec_helper'
describe 'projects/merge_requests/edit.html.haml' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
let(:fork_project) { create(:project, forked_from_project: project) }
let(:unlink_project) { Projects::UnlinkForkService.new(fork_project, user) }
+ let(:milestone) { create(:milestone, project: project) }
let(:closed_merge_request) do
create(:closed_merge_request,
source_project: fork_project,
target_project: project,
- author: user)
+ author: user,
+ assignee: user,
+ milestone: milestone)
end
before do
diff --git a/spec/views/projects/merge_requests/show.html.haml_spec.rb b/spec/views/projects/merge_requests/show.html.haml_spec.rb
index fe0780e72df..33cabd14913 100644
--- a/spec/views/projects/merge_requests/show.html.haml_spec.rb
+++ b/spec/views/projects/merge_requests/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/merge_requests/show.html.haml' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -41,4 +41,17 @@ describe 'projects/merge_requests/show.html.haml' do
expect(rendered).to have_css('a', visible: false, text: 'Close')
end
end
+
+ context 'when the merge request is open' do
+ it 'closes the merge request if the source project does not exist' do
+ closed_merge_request.update_attributes(state: 'open')
+ fork_project.destroy
+
+ render
+
+ expect(closed_merge_request.reload.state).to eq('closed')
+ expect(rendered).to have_css('a', visible: false, text: 'Reopen')
+ expect(rendered).to have_css('a', visible: false, text: 'Close')
+ end
+ end
end
diff --git a/spec/views/projects/notes/_form.html.haml_spec.rb b/spec/views/projects/notes/_form.html.haml_spec.rb
index 932d6756ad2..b14b1ece2d0 100644
--- a/spec/views/projects/notes/_form.html.haml_spec.rb
+++ b/spec/views/projects/notes/_form.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/notes/_form' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:user) { create(:user) }
let(:project) { create(:empty_project) }
diff --git a/spec/views/projects/pipelines/show.html.haml_spec.rb b/spec/views/projects/pipelines/show.html.haml_spec.rb
index ac7f3ffb157..bf027499c94 100644
--- a/spec/views/projects/pipelines/show.html.haml_spec.rb
+++ b/spec/views/projects/pipelines/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/pipelines/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:pipeline) { create(:ci_empty_pipeline, project: project, sha: project.commit.id) }
diff --git a/spec/views/projects/tree/show.html.haml_spec.rb b/spec/views/projects/tree/show.html.haml_spec.rb
index 0f3fc1ee1ac..c381b1a86df 100644
--- a/spec/views/projects/tree/show.html.haml_spec.rb
+++ b/spec/views/projects/tree/show.html.haml_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe 'projects/tree/show' do
- include Devise::TestHelpers
+ include Devise::Test::ControllerHelpers
let(:project) { create(:project) }
let(:repository) { project.repository }
diff --git a/spec/workers/expire_build_artifacts_worker_spec.rb b/spec/workers/expire_build_artifacts_worker_spec.rb
index 7d6668920c0..73cbadc13d9 100644
--- a/spec/workers/expire_build_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_artifacts_worker_spec.rb
@@ -5,65 +5,42 @@ describe ExpireBuildArtifactsWorker do
let(:worker) { described_class.new }
+ before { Sidekiq::Worker.clear_all }
+
describe '#perform' do
before { build }
- subject! { worker.perform }
+ subject! do
+ Sidekiq::Testing.fake! { worker.perform }
+ end
context 'with expired artifacts' do
let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
- it 'does expire' do
- expect(build.reload.artifacts_expired?).to be_truthy
- end
-
- it 'does remove files' do
- expect(build.reload.artifacts_file.exists?).to be_falsey
- end
-
- it 'does nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).to be_nil
+ it 'enqueues that build' do
+ expect(jobs_enqueued.size).to eq(1)
+ expect(jobs_enqueued[0]["args"]).to eq([build.id])
end
end
context 'with not yet expired artifacts' do
let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
- it 'does not expire' do
- expect(build.reload.artifacts_expired?).to be_falsey
- end
-
- it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
- end
-
- it 'does not nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).not_to be_nil
+ it 'does not enqueue that build' do
+ expect(jobs_enqueued.size).to eq(0)
end
end
context 'without expire date' do
let(:build) { create(:ci_build, :artifacts) }
- it 'does not expire' do
- expect(build.reload.artifacts_expired?).to be_falsey
- end
-
- it 'does not remove files' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
- end
-
- it 'does not nullify artifacts_file column' do
- expect(build.reload.artifacts_file_identifier).not_to be_nil
+ it 'does not enqueue that build' do
+ expect(jobs_enqueued.size).to eq(0)
end
end
- context 'for expired artifacts' do
- let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
-
- it 'is still expired' do
- expect(build.reload.artifacts_expired?).to be_truthy
- end
+ def jobs_enqueued
+ Sidekiq::Queues.jobs_by_worker['ExpireBuildInstanceArtifactsWorker']
end
end
end
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
new file mode 100644
index 00000000000..2b140f2ba28
--- /dev/null
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe ExpireBuildInstanceArtifactsWorker do
+ include RepoHelpers
+
+ let(:worker) { described_class.new }
+
+ describe '#perform' do
+ before { build }
+
+ subject! { worker.perform(build.id) }
+
+ context 'with expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'does expire' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+
+ it 'does remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_falsey
+ end
+
+ it 'does nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).to be_nil
+ end
+ end
+
+ context 'with not yet expired artifacts' do
+ let(:build) { create(:ci_build, :artifacts, artifacts_expire_at: Time.now + 7.days) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
+ end
+
+ context 'without expire date' do
+ let(:build) { create(:ci_build, :artifacts) }
+
+ it 'does not expire' do
+ expect(build.reload.artifacts_expired?).to be_falsey
+ end
+
+ it 'does not remove files' do
+ expect(build.reload.artifacts_file.exists?).to be_truthy
+ end
+
+ it 'does not nullify artifacts_file column' do
+ expect(build.reload.artifacts_file_identifier).not_to be_nil
+ end
+ end
+
+ context 'for expired artifacts' do
+ let(:build) { create(:ci_build, artifacts_expire_at: Time.now - 7.days) }
+
+ it 'is still expired' do
+ expect(build.reload.artifacts_expired?).to be_truthy
+ end
+ end
+ end
+end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index 1d2cf7acddd..ffeaafe654a 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -79,7 +79,9 @@ describe PostReceive do
end
it "does not run if the author is not in the project" do
- allow(Key).to receive(:find_by).with(hash_including(id: anything())) { nil }
+ allow_any_instance_of(Gitlab::GitPostReceive).
+ to receive(:identify_using_ssh_key).
+ and_return(nil)
expect(project).not_to receive(:execute_hooks)
diff --git a/spec/workers/process_pipeline_worker_spec.rb b/spec/workers/process_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..7b5f98d5763
--- /dev/null
+++ b/spec/workers/process_pipeline_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe ProcessPipelineWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'processes pipeline' do
+ expect_any_instance_of(Ci::Pipeline).to receive(:process!)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/spec/workers/trending_projects_worker_spec.rb b/spec/workers/trending_projects_worker_spec.rb
new file mode 100644
index 00000000000..c3c6fdcf2d5
--- /dev/null
+++ b/spec/workers/trending_projects_worker_spec.rb
@@ -0,0 +1,11 @@
+require 'spec_helper'
+
+describe TrendingProjectsWorker do
+ describe '#perform' do
+ it 'refreshes the trending projects' do
+ expect(TrendingProject).to receive(:refresh!)
+
+ described_class.new.perform
+ end
+ end
+end
diff --git a/spec/workers/update_pipeline_worker_spec.rb b/spec/workers/update_pipeline_worker_spec.rb
new file mode 100644
index 00000000000..fadc42b22f0
--- /dev/null
+++ b/spec/workers/update_pipeline_worker_spec.rb
@@ -0,0 +1,22 @@
+require 'spec_helper'
+
+describe UpdatePipelineWorker do
+ describe '#perform' do
+ context 'when pipeline exists' do
+ let(:pipeline) { create(:ci_pipeline) }
+
+ it 'updates pipeline status' do
+ expect_any_instance_of(Ci::Pipeline).to receive(:update_status)
+
+ described_class.new.perform(pipeline.id)
+ end
+ end
+
+ context 'when pipeline does not exist' do
+ it 'does not raise exception' do
+ expect { described_class.new.perform(123) }
+ .not_to raise_error
+ end
+ end
+ end
+end
diff --git a/vendor/gitignore/Erlang.gitignore b/vendor/gitignore/Erlang.gitignore
index 8e46d5a07f8..3826c85736f 100644
--- a/vendor/gitignore/Erlang.gitignore
+++ b/vendor/gitignore/Erlang.gitignore
@@ -4,7 +4,7 @@ deps
*.beam
*.plt
erl_crash.dump
-ebin
+ebin/*.beam
rel/example_project
.concrete/DEV_MODE
.rebar
diff --git a/vendor/gitignore/Global/Ansible.gitignore b/vendor/gitignore/Global/Ansible.gitignore
new file mode 100644
index 00000000000..a8b42eb6eed
--- /dev/null
+++ b/vendor/gitignore/Global/Ansible.gitignore
@@ -0,0 +1 @@
+*.retry
diff --git a/vendor/gitignore/Global/Linux.gitignore b/vendor/gitignore/Global/Linux.gitignore
index cc9586893b6..b56bf65d855 100644
--- a/vendor/gitignore/Global/Linux.gitignore
+++ b/vendor/gitignore/Global/Linux.gitignore
@@ -8,3 +8,6 @@
# Linux trash folder which might appear on any partition or disk
.Trash-*
+
+# .nfs files are created when an open file is removed but is still being accessed
+.nfs*
diff --git a/vendor/gitignore/Go.gitignore b/vendor/gitignore/Go.gitignore
index cd0d5d1e2f4..397a0ed4acb 100644
--- a/vendor/gitignore/Go.gitignore
+++ b/vendor/gitignore/Go.gitignore
@@ -25,3 +25,6 @@ _testmain.go
# Output of the go coverage tool, specifically when used with LiteIDE
*.out
+
+# external packages folder
+vendor/
diff --git a/vendor/gitignore/Node.gitignore b/vendor/gitignore/Node.gitignore
index bf7525f9912..bc7fc55724c 100644
--- a/vendor/gitignore/Node.gitignore
+++ b/vendor/gitignore/Node.gitignore
@@ -39,3 +39,6 @@ jspm_packages
# Optional REPL history
.node_repl_history
+
+# Output of 'npm pack'
+*.tgz
diff --git a/vendor/gitignore/TeX.gitignore b/vendor/gitignore/TeX.gitignore
index 34f999df3e7..f620fad23eb 100644
--- a/vendor/gitignore/TeX.gitignore
+++ b/vendor/gitignore/TeX.gitignore
@@ -192,3 +192,6 @@ TSWLatexianTemp*
# KBibTeX
*~[0-9]*
+
+# auto folder when using emacs and auctex
+/auto/*
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index d56f8b53288..1b86e7ec918 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -110,6 +110,10 @@ _TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
+# Visual Studio code coverage results
+*.coverage
+*.coveragexml
+
# NCrunch
_NCrunch_*
.*crunch*.local.xml
@@ -189,6 +193,7 @@ ClientBin/
*~
*.dbmdl
*.dbproj.schemaview
+*.jfm
*.pfx
*.publishsettings
node_modules/
@@ -258,3 +263,6 @@ paket-files/
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
+
+# Cake - Uncomment if you are using it
+# tools/
diff --git a/vendor/gitlab-ci-yml/.gitlab-ci.yml b/vendor/gitlab-ci-yml/.gitlab-ci.yml
new file mode 100644
index 00000000000..18b14554887
--- /dev/null
+++ b/vendor/gitlab-ci-yml/.gitlab-ci.yml
@@ -0,0 +1,4 @@
+image: ruby:2.3-alpine
+
+test:
+ script: ruby verify_templates.rb
diff --git a/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
new file mode 100644
index 00000000000..263c4c19999
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Gradle.gitlab-ci.yml
@@ -0,0 +1,34 @@
+# This template uses the java:8 docker image because there isn't any
+# official Gradle image at this moment
+#
+# This is the Gradle build system for JVM applications
+# https://gradle.org/
+# https://github.com/gradle/gradle
+image: java:8
+
+# Make the gradle wrapper executable. This essentially downloads a copy of
+# Gradle to build the project with.
+# https://docs.gradle.org/current/userguide/gradle_wrapper.html
+# It is expected that any modern gradle project has a wrapper
+before_script:
+ - chmod +x gradlew
+
+# We redirect the gradle user home using -g so that it caches the
+# wrapper and dependencies.
+# https://docs.gradle.org/current/userguide/gradle_command_line.html
+#
+# Unfortunately it also caches the build output so
+# cleaning removes reminants of any cached builds.
+# The assemble task actually builds the project.
+# If it fails here, the tests can't run.
+build:
+ stage: build
+ script:
+ - ./gradlew -g /cache/.gradle clean assemble
+ allow_failure: false
+
+# Use the generated build output to run the tests.
+test:
+ stage: test
+ script:
+ - ./gradlew -g /cache./gradle check
diff --git a/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml
new file mode 100644
index 00000000000..140cb4635f3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Julia.gitlab-ci.yml
@@ -0,0 +1,54 @@
+# An example .gitlab-ci.yml file to test (and optionally report the coverage
+# results of) your [Julia][1] packages. Please refer to the [documentation][2]
+# for more information about package development in Julia.
+#
+# Here, it is assumed that your Julia package is named `MyPackage`. Change it to
+# whatever name you have given to your package.
+#
+# [1]: http://julialang.org/
+# [2]: http://julia.readthedocs.org/
+
+# Below is the template to run your tests in Julia
+.test_template: &test_definition
+ # Uncomment below if you would like to run the tests on specific references
+ # only, such as the branches `master`, `development`, etc.
+ # only:
+ # - master
+ # - development
+ script:
+ # Let's run the tests. Substitute `coverage = false` below, if you do not
+ # want coverage results.
+ - /opt/julia/bin/julia -e 'Pkg.clone(pwd()); Pkg.test("MyPackage",
+ coverage = true)'
+ # Comment out below if you do not want coverage results.
+ - /opt/julia/bin/julia -e 'Pkg.add("Coverage"); cd(Pkg.dir("MyPackage"));
+ using Coverage; cl, tl = get_summary(process_folder());
+ println("(", cl/tl*100, "%) covered")'
+
+# Name a test and select an appropriate image.
+test:0.4.6:
+ image: julialang/julia:v0.4.6
+ <<: *test_definition
+
+# Maybe you would like to test your package against the development branch:
+test:0.5.0-dev:
+ image: julialang/julia:v0.5.0-dev
+ # ... allowing for failures, since we are testing against the development
+ # branch:
+ allow_failure: true
+ <<: *test_definition
+
+# REMARK: Do not forget to enable the coverage feature for your project, if you
+# are using code coverage reporting above. This can be done by
+#
+# - Navigating to the `CI/CD Pipelines` settings of your project,
+# - Copying and pasting the default `Simplecov` regex example provided, i.e.,
+# `\(\d+.\d+\%\) covered` in the `test coverage parsing` textfield.
+#
+# WARNING: This template is using the `julialang/julia` images from [Docker
+# Hub][3]. One can use custom Julia images and/or the official ones found
+# in the same place. However, care must be taken to correctly locate the binary
+# file (`/opt/julia/bin/julia` above), which is usually given on the image's
+# description page.
+#
+# [3]: http://hub.docker.com/