summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--CHANGELOG23
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock8
-rw-r--r--app/assets/javascripts/LabelManager.js.coffee5
-rw-r--r--app/assets/javascripts/api.js.coffee7
-rw-r--r--app/assets/javascripts/application.js.coffee73
-rw-r--r--app/assets/javascripts/blob/blob_ci_yaml.js.coffee23
-rw-r--r--app/assets/javascripts/blob/edit_blob.js.coffee1
-rw-r--r--app/assets/javascripts/dispatcher.js.coffee9
-rw-r--r--app/assets/javascripts/gl_dropdown.js.coffee15
-rw-r--r--app/assets/javascripts/gl_form.js.coffee3
-rw-r--r--app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee6
-rw-r--r--app/assets/javascripts/labels_select.js.coffee25
-rw-r--r--app/assets/javascripts/lib/text_utility.js.coffee79
-rw-r--r--app/assets/javascripts/notes.js.coffee5
-rw-r--r--app/assets/javascripts/notifications_dropdown.js.coffee25
-rw-r--r--app/assets/javascripts/notifications_form.js.coffee49
-rw-r--r--app/assets/javascripts/profile.js.coffee4
-rw-r--r--app/assets/javascripts/project.js.coffee54
-rw-r--r--app/assets/javascripts/right_sidebar.js.coffee18
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/blank.scss23
-rw-r--r--app/assets/stylesheets/framework/blocks.scss16
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss10
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss30
-rw-r--r--app/assets/stylesheets/framework/mixins.scss14
-rw-r--r--app/assets/stylesheets/framework/mobile.scss13
-rw-r--r--app/assets/stylesheets/framework/nav.scss22
-rw-r--r--app/assets/stylesheets/framework/panels.scss4
-rw-r--r--app/assets/stylesheets/framework/selects.scss5
-rw-r--r--app/assets/stylesheets/framework/sidebar.scss1
-rw-r--r--app/assets/stylesheets/pages/commits.scss12
-rw-r--r--app/assets/stylesheets/pages/diff.scss5
-rw-r--r--app/assets/stylesheets/pages/editor.scss11
-rw-r--r--app/assets/stylesheets/pages/events.scss7
-rw-r--r--app/assets/stylesheets/pages/help.scss7
-rw-r--r--app/assets/stylesheets/pages/issuable.scss7
-rw-r--r--app/assets/stylesheets/pages/labels.scss4
-rw-r--r--app/assets/stylesheets/pages/merge_requests.scss15
-rw-r--r--app/assets/stylesheets/pages/note_form.scss17
-rw-r--r--app/assets/stylesheets/pages/notes.scss23
-rw-r--r--app/assets/stylesheets/pages/projects.scss56
-rw-r--r--app/assets/stylesheets/pages/stat_graph.scss22
-rw-r--r--app/assets/stylesheets/pages/todos.scss4
-rw-r--r--app/controllers/admin/appearances_controller.rb1
-rw-r--r--app/controllers/admin/runner_projects_controller.rb11
-rw-r--r--app/controllers/application_controller.rb4
-rw-r--r--app/controllers/concerns/membership_actions.rb31
-rw-r--r--app/controllers/groups/group_members_controller.rb8
-rw-r--r--app/controllers/groups/notification_settings_controller.rb16
-rw-r--r--app/controllers/notification_settings_controller.rb36
-rw-r--r--app/controllers/profiles/notifications_controller.rb18
-rw-r--r--app/controllers/projects/application_controller.rb2
-rw-r--r--app/controllers/projects/notification_settings_controller.rb16
-rw-r--r--app/controllers/projects/pipelines_controller.rb2
-rw-r--r--app/controllers/projects/project_members_controller.rb8
-rw-r--r--app/controllers/projects/runner_projects_controller.rb4
-rw-r--r--app/controllers/projects/runners_controller.rb7
-rw-r--r--app/controllers/projects/todos_controller.rb22
-rw-r--r--app/controllers/projects_controller.rb28
-rw-r--r--app/helpers/application_helper.rb18
-rw-r--r--app/helpers/blob_helper.rb18
-rw-r--r--app/helpers/branches_helper.rb2
-rw-r--r--app/helpers/gitlab_markdown_helper.rb15
-rw-r--r--app/helpers/issuables_helper.rb6
-rw-r--r--app/helpers/members_helper.rb6
-rw-r--r--app/helpers/notifications_helper.rb28
-rw-r--r--app/helpers/todos_helper.rb2
-rw-r--r--app/mailers/emails/members.rb5
-rw-r--r--app/models/ci/build.rb8
-rw-r--r--app/models/ci/pipeline.rb8
-rw-r--r--app/models/ci/runner.rb23
-rw-r--r--app/models/commit.rb26
-rw-r--r--app/models/commit_status.rb2
-rw-r--r--app/models/concerns/participable.rb10
-rw-r--r--app/models/concerns/referable.rb4
-rw-r--r--app/models/issue.rb4
-rw-r--r--app/models/key.rb2
-rw-r--r--app/models/member.rb7
-rw-r--r--app/models/members/group_member.rb12
-rw-r--r--app/models/members/project_member.rb12
-rw-r--r--app/models/merge_request.rb4
-rw-r--r--app/models/notification_setting.rb37
-rw-r--r--app/models/repository.rb11
-rw-r--r--app/models/user.rb3
-rw-r--r--app/services/ci/register_build_service.rb2
-rw-r--r--app/services/members/destroy_service.rb21
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/notification_service.rb137
-rw-r--r--app/services/projects/import_export/export_service.rb2
-rw-r--r--app/views/admin/appearances/_form.html.haml2
-rw-r--r--app/views/admin/appearances/preview.html.haml34
-rw-r--r--app/views/admin/appearances/show.html.haml2
-rw-r--r--app/views/admin/application_settings/show.html.haml1
-rw-r--r--app/views/admin/dashboard/_head.html.haml4
-rw-r--r--app/views/admin/groups/show.html.haml33
-rw-r--r--app/views/admin/projects/show.html.haml49
-rw-r--r--app/views/admin/runners/index.html.haml124
-rw-r--r--app/views/admin/runners/show.html.haml4
-rw-r--r--app/views/dashboard/_projects_head.html.haml2
-rw-r--r--app/views/groups/group_members/index.html.haml3
-rw-r--r--app/views/groups/show.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml6
-rw-r--r--app/views/import/github/status.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml2
-rw-r--r--app/views/layouts/nav/_admin.html.haml95
-rw-r--r--app/views/layouts/nav/_admin_settings.html.haml31
-rw-r--r--app/views/layouts/nav/_group.html.haml6
-rw-r--r--app/views/layouts/nav/_group_settings.html.haml36
-rw-r--r--app/views/layouts/nav/_profile.html.haml6
-rw-r--r--app/views/layouts/nav/_project.html.haml21
-rw-r--r--app/views/layouts/nav/_project_settings.html.haml2
-rw-r--r--app/views/notify/project_was_not_exported_email.html.haml2
-rw-r--r--app/views/notify/project_was_not_exported_email.text.erb6
-rw-r--r--app/views/notify/project_was_not_exported_email.text.haml6
-rw-r--r--app/views/profiles/notifications/_group_settings.html.haml3
-rw-r--r--app/views/profiles/notifications/_project_settings.html.haml3
-rw-r--r--app/views/profiles/notifications/show.html.haml13
-rw-r--r--app/views/projects/_home_panel.html.haml12
-rw-r--r--app/views/projects/_md_preview.html.haml15
-rw-r--r--app/views/projects/badges/index.html.haml2
-rw-r--r--app/views/projects/blob/_editor.html.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml6
-rw-r--r--app/views/projects/buttons/_notifications.html.haml11
-rw-r--r--app/views/projects/ci/pipelines/_pipeline.html.haml4
-rw-r--r--app/views/projects/commits/_commit.html.haml47
-rw-r--r--app/views/projects/commits/_head.html.haml6
-rw-r--r--app/views/projects/compare/index.html.haml2
-rw-r--r--app/views/projects/compare/show.html.haml36
-rw-r--r--app/views/projects/container_registry/_tag.html.haml6
-rw-r--r--app/views/projects/environments/index.html.haml16
-rw-r--r--app/views/projects/environments/new.html.haml5
-rw-r--r--app/views/projects/environments/show.html.haml12
-rw-r--r--app/views/projects/forks/index.html.haml6
-rw-r--r--app/views/projects/graphs/_head.html.haml26
-rw-r--r--app/views/projects/graphs/ci.html.haml25
-rw-r--r--app/views/projects/graphs/commits.html.haml90
-rw-r--r--app/views/projects/graphs/languages.html.haml36
-rw-r--r--app/views/projects/graphs/show.html.haml48
-rw-r--r--app/views/projects/issues/_discussion.html.haml4
-rw-r--r--app/views/projects/merge_requests/_discussion.html.haml4
-rw-r--r--app/views/projects/network/show.html.haml2
-rw-r--r--app/views/projects/notes/_edit_form.html.haml2
-rw-r--r--app/views/projects/notes/_form.html.haml2
-rw-r--r--app/views/projects/notes/_hints.html.haml2
-rw-r--r--app/views/projects/notes/_note.html.haml4
-rw-r--r--app/views/projects/project_members/_group_members.html.haml3
-rw-r--r--app/views/projects/project_members/_shared_group_members.html.haml5
-rw-r--r--app/views/projects/project_members/_team.html.haml3
-rw-r--r--app/views/projects/project_members/index.html.haml2
-rw-r--r--app/views/projects/runners/_form.html.haml6
-rw-r--r--app/views/projects/runners/_runner.html.haml6
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml10
-rw-r--r--app/views/projects/runners/show.html.haml3
-rw-r--r--app/views/projects/show.html.haml4
-rw-r--r--app/views/projects/tags/_tag.html.haml2
-rw-r--r--app/views/projects/wikis/_main_links.html.haml3
-rw-r--r--app/views/projects/wikis/_nav.html.haml11
-rw-r--r--app/views/projects/wikis/_new.html.haml31
-rw-r--r--app/views/projects/wikis/edit.html.haml31
-rw-r--r--app/views/projects/wikis/git_access.html.haml52
-rw-r--r--app/views/projects/wikis/history.html.haml66
-rw-r--r--app/views/projects/wikis/pages.html.haml18
-rw-r--r--app/views/projects/wikis/show.html.haml34
-rw-r--r--app/views/shared/_event_filter.html.haml6
-rw-r--r--app/views/shared/_ref_switcher.html.haml9
-rw-r--r--app/views/shared/issuable/_sidebar.html.haml10
-rw-r--r--app/views/shared/members/_access_request_buttons.html.haml20
-rw-r--r--app/views/shared/members/_member.html.haml3
-rw-r--r--app/views/shared/members/_requests.html.haml2
-rw-r--r--app/views/shared/milestones/_summary.html.haml7
-rw-r--r--app/views/shared/notifications/_button.html.haml25
-rw-r--r--app/views/shared/notifications/_custom_notifications.html.haml31
-rw-r--r--app/views/shared/notifications/_notification_dropdown.html.haml13
-rw-r--r--app/views/shared/projects/_list.html.haml6
-rw-r--r--config/initializers/1_settings.rb2
-rw-r--r--config/initializers/sidekiq.rb4
-rw-r--r--config/routes.rb15
-rw-r--r--db/migrate/20160509091049_add_locked_to_ci_runner.rb13
-rw-r--r--db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb9
-rw-r--r--db/migrate/20160616102642_remove_duplicated_keys.rb19
-rw-r--r--db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb21
-rw-r--r--db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb13
-rw-r--r--db/migrate/20160617301627_add_events_to_notification_settings.rb7
-rw-r--r--db/migrate/20160620115026_add_index_on_runners_locked.rb12
-rw-r--r--db/schema.rb6
-rw-r--r--doc/README.md23
-rw-r--r--doc/administration/container_registry.md86
-rw-r--r--doc/administration/raketasks/project_import_export.md33
-rw-r--r--doc/api/README.md1
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/environments.md58
-rw-r--r--doc/ci/examples/README.md3
-rw-r--r--doc/ci/examples/test-scala-application.md47
-rw-r--r--doc/ci/pipelines.md38
-rw-r--r--doc/ci/quick_start/README.md64
-rw-r--r--doc/ci/quick_start/img/pipelines_status.pngbin0 -> 89387 bytes
-rw-r--r--doc/ci/runners/README.md8
-rw-r--r--doc/ci/yaml/README.md69
-rw-r--r--doc/development/doc_styleguide.md56
-rw-r--r--doc/project_services/emails_on_push.md17
-rw-r--r--doc/project_services/img/emails_on_push_service.pngbin0 -> 98160 bytes
-rw-r--r--doc/project_services/project_services.md2
-rw-r--r--doc/update/8.6-to-8.7.md2
-rw-r--r--doc/user/project/settings/img/import_export_download_export.pngbin0 -> 85600 bytes
-rw-r--r--doc/user/project/settings/img/import_export_export_button.pngbin0 -> 84637 bytes
-rw-r--r--doc/user/project/settings/img/import_export_mail_link.pngbin0 -> 44012 bytes
-rw-r--r--doc/user/project/settings/img/import_export_new_project.pngbin0 -> 43574 bytes
-rw-r--r--doc/user/project/settings/img/import_export_select_file.pngbin0 -> 46292 bytes
-rw-r--r--doc/user/project/settings/img/settings_edit_button.pngbin0 -> 19392 bytes
-rw-r--r--doc/user/project/settings/import_export.md73
-rw-r--r--doc/workflow/add-user/add-user.md24
-rw-r--r--doc/workflow/add-user/img/access_requests_management.pngbin0 -> 15105 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_accept.pngbin10833 -> 22961 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_ready.pngbin16177 -> 40305 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_email_search.pngbin15889 -> 45884 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_give_permissions.pngbin22089 -> 56480 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_import_members_from_another_project.pngbin18897 -> 38874 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_imported_members.pngbin23897 -> 37873 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_list_members.pngbin15732 -> 24427 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_members_menu.pngbin8295 -> 42319 bytes
-rw-r--r--doc/workflow/add-user/img/add_user_search_people.pngbin13518 -> 39941 bytes
-rw-r--r--doc/workflow/add-user/img/request_access_button.pngbin0 -> 36588 bytes
-rw-r--r--doc/workflow/add-user/img/withdraw_access_request_button.pngbin0 -> 37960 bytes
-rw-r--r--doc/workflow/award_emoji.md45
-rw-r--r--doc/workflow/award_emoji.pngbin6620 -> 16926 bytes
-rw-r--r--doc/workflow/groups.md22
-rw-r--r--doc/workflow/groups/access_requests_management.pngbin0 -> 15193 bytes
-rw-r--r--doc/workflow/groups/request_access_button.pngbin0 -> 30470 bytes
-rw-r--r--doc/workflow/groups/withdraw_access_request_button.pngbin0 -> 31681 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_awarded.pngbin0 -> 64317 bytes
-rw-r--r--doc/workflow/img/award_emoji_comment_picker.pngbin0 -> 250861 bytes
-rw-r--r--doc/workflow/notifications.md6
-rw-r--r--doc/workflow/notifications/settings.pngbin114727 -> 90986 bytes
-rw-r--r--doc/workflow/shortcuts.pngbin90936 -> 108255 bytes
-rw-r--r--features/admin/groups.feature7
-rw-r--r--features/dashboard/group.feature44
-rw-r--r--features/project/active_tab.feature30
-rw-r--r--features/project/shortcuts.feature6
-rw-r--r--features/steps/admin/groups.rb8
-rw-r--r--features/steps/dashboard/group.rb42
-rw-r--r--features/steps/profile/notifications.rb6
-rw-r--r--features/steps/project/network_graph.rb20
-rw-r--r--features/steps/project/project.rb6
-rw-r--r--features/steps/project/project_find_file.rb4
-rw-r--r--features/steps/project/source/browse_files.rb14
-rw-r--r--features/steps/shared/project_tab.rb4
-rw-r--r--lib/api/api.rb2
-rw-r--r--lib/api/entities.rb11
-rw-r--r--lib/api/gitignores.rb29
-rw-r--r--lib/api/internal.rb2
-rw-r--r--lib/api/runners.rb12
-rw-r--r--lib/api/templates.rb36
-rw-r--r--lib/banzai.rb4
-rw-r--r--lib/banzai/filter/abstract_reference_filter.rb3
-rw-r--r--lib/banzai/filter/relative_link_filter.rb51
-rw-r--r--lib/banzai/filter/wiki_link_filter.rb2
-rw-r--r--lib/banzai/renderer.rb8
-rw-r--r--lib/ci/api/runners.rb9
-rw-r--r--lib/container_registry/tag.rb1
-rw-r--r--lib/gitlab/access.rb2
-rw-r--r--lib/gitlab/backend/grack_auth.rb2
-rw-r--r--lib/gitlab/database.rb5
-rw-r--r--lib/gitlab/email/message/repository_push.rb2
-rw-r--r--lib/gitlab/github_import/importer.rb23
-rw-r--r--lib/gitlab/gitignore.rb56
-rw-r--r--lib/gitlab/import_export.rb2
-rw-r--r--lib/gitlab/lfs/response.rb7
-rw-r--r--lib/gitlab/lfs/router.rb7
-rw-r--r--lib/gitlab/template/base_template.rb67
-rw-r--r--lib/gitlab/template/gitignore.rb22
-rw-r--r--lib/gitlab/template/gitlab_ci_yml.rb27
-rw-r--r--lib/tasks/gitlab/import_export.rake13
-rw-r--r--lib/tasks/gitlab/update_gitignore.rake46
-rw-r--r--lib/tasks/gitlab/update_templates.rake54
-rw-r--r--spec/controllers/groups/group_members_controller_spec.rb6
-rw-r--r--spec/controllers/groups/notification_settings_controller_spec.rb32
-rw-r--r--spec/controllers/notification_settings_controller_spec.rb125
-rw-r--r--spec/controllers/projects/notification_settings_controller_spec.rb52
-rw-r--r--spec/controllers/projects/project_members_controller_spec.rb8
-rw-r--r--spec/controllers/projects/todo_controller_spec.rb102
-rw-r--r--spec/controllers/projects_controller_spec.rb20
-rw-r--r--spec/factories/ci/pipelines.rb (renamed from spec/factories/ci/commits.rb)0
-rw-r--r--spec/features/admin/admin_runners_spec.rb34
-rw-r--r--spec/features/container_registry_spec.rb3
-rw-r--r--spec/features/environments_spec.rb6
-rw-r--r--spec/features/groups/members/last_owner_cannot_leave_group_spec.rb16
-rw-r--r--spec/features/groups/members/member_leaves_group_spec.rb21
-rw-r--r--spec/features/groups/members/owner_manages_access_requests_spec.rb2
-rw-r--r--spec/features/groups/members/user_requests_access_spec.rb1
-rw-r--r--spec/features/issues/bulk_assignment_labels_spec.rb (renamed from spec/features/issues/bulk_assigment_labels_spec.rb)3
-rw-r--r--spec/features/issues/todo_spec.rb10
-rw-r--r--spec/features/issues_spec.rb2
-rw-r--r--spec/features/projects/badges/list_spec.rb8
-rw-r--r--spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb30
-rw-r--r--spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb50
-rw-r--r--spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb21
-rw-r--r--spec/features/projects/members/master_manages_access_requests_spec.rb2
-rw-r--r--spec/features/projects/members/member_leaves_project_spec.rb19
-rw-r--r--spec/features/projects/members/owner_cannot_leave_project_spec.rb16
-rw-r--r--spec/features/projects/members/user_requests_access_spec.rb1
-rw-r--r--spec/features/projects_spec.rb16
-rw-r--r--spec/fixtures/container_registry/tag_manifest.json17
-rw-r--r--spec/helpers/application_helper_spec.rb45
-rw-r--r--spec/helpers/members_helper_spec.rb48
-rw-r--r--spec/javascripts/issue_spec.js.coffee13
-rw-r--r--spec/lib/banzai/filter/abstract_link_filter_spec.rb2
-rw-r--r--spec/lib/banzai/filter/issue_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/merge_request_reference_filter_spec.rb6
-rw-r--r--spec/lib/banzai/filter/relative_link_filter_spec.rb7
-rw-r--r--spec/lib/banzai/filter/wiki_link_filter_spec.rb26
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb561
-rw-r--r--spec/lib/gitlab/template/gitignore_spec.rb (renamed from spec/lib/gitlab/gitignore_spec.rb)6
-rw-r--r--spec/mailers/notify_spec.rb61
-rw-r--r--spec/models/build_spec.rb127
-rw-r--r--spec/models/ci/runner_spec.rb241
-rw-r--r--spec/models/commit_spec.rb12
-rw-r--r--spec/models/commit_status_spec.rb19
-rw-r--r--spec/models/concerns/participable_spec.rb10
-rw-r--r--spec/models/key_spec.rb2
-rw-r--r--spec/models/member_spec.rb12
-rw-r--r--spec/models/members/group_member_spec.rb10
-rw-r--r--spec/models/members/project_member_spec.rb10
-rw-r--r--spec/models/notification_setting_spec.rb25
-rw-r--r--spec/requests/api/builds_spec.rb11
-rw-r--r--spec/requests/api/gitignores_spec.rb29
-rw-r--r--spec/requests/api/projects_spec.rb13
-rw-r--r--spec/requests/api/runners_spec.rb24
-rw-r--r--spec/requests/api/templates_spec.rb52
-rw-r--r--spec/services/members/destroy_service_spec.rb71
-rw-r--r--spec/services/notification_service_spec.rb117
-rw-r--r--spec/workers/merge_worker_spec.rb2
-rw-r--r--vendor/gitignore/Android.gitignore3
-rw-r--r--vendor/gitignore/C++.gitignore1
-rw-r--r--vendor/gitignore/CMake.gitignore1
-rw-r--r--vendor/gitignore/D.gitignore4
-rw-r--r--vendor/gitignore/Global/Bazaar.gitignore2
-rw-r--r--vendor/gitignore/Global/OSX.gitignore3
-rw-r--r--vendor/gitignore/Global/README.md10
-rw-r--r--vendor/gitignore/Global/SublimeText.gitignore13
-rw-r--r--vendor/gitignore/Haskell.gitignore1
-rw-r--r--vendor/gitignore/Julia.gitignore4
-rw-r--r--vendor/gitignore/LICENSE19
-rw-r--r--vendor/gitignore/Laravel.gitignore1
-rw-r--r--vendor/gitignore/Objective-C.gitignore9
-rw-r--r--vendor/gitignore/Qt.gitignore2
-rw-r--r--vendor/gitignore/README.md14
-rw-r--r--vendor/gitignore/Rails.gitignore4
-rw-r--r--vendor/gitignore/Swift.gitignore2
-rw-r--r--vendor/gitignore/UnrealEngine.gitignore1
-rw-r--r--vendor/gitignore/VisualStudio.gitignore1
-rw-r--r--vendor/gitlab-ci-yml/Docker.gitlab-ci.yml7
-rw-r--r--vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml18
-rw-r--r--vendor/gitlab-ci-yml/LICENSE21
-rw-r--r--vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml27
-rw-r--r--vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml16
-rw-r--r--vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml13
-rw-r--r--vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml16
-rw-r--r--vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml25
-rw-r--r--vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml11
-rw-r--r--vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml25
-rw-r--r--vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml24
-rw-r--r--vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml17
-rw-r--r--vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml27
-rw-r--r--vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml12
-rw-r--r--vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml15
-rw-r--r--vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml10
-rw-r--r--vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml30
370 files changed, 4763 insertions, 2128 deletions
diff --git a/CHANGELOG b/CHANGELOG
index de9d71dae97..222563d355c 100644
--- a/CHANGELOG
+++ b/CHANGELOG
@@ -1,23 +1,30 @@
Please view this file on the master branch, on stable branches it's out of date.
v 8.9.0 (unreleased)
+ - Fix builds API response not including commit data
- Fix error when CI job variables key specified but not defined
- Fix pipeline status when there are no builds in pipeline
- Fix Error 500 when using closes_issues API with an external issue tracker
- Add more information into RSS feed for issues (Alexander Matyushentsev)
- Bulk assign/unassign labels to issues.
- Ability to prioritize labels !4009 / !3205 (Thijs Wouters)
+ - Show Star and Fork buttons on mobile.
+ - Performance improvements on RelativeLinkFilter
- Fix endless redirections when accessing user OAuth applications when they are disabled
- Allow enabling wiki page events from Webhook management UI
- Bump rouge to 1.11.0
+ - Fix MR-auto-close text added to description
- Fix issue with arrow keys not working in search autocomplete dropdown
- Fix an issue where note polling stopped working if a window was in the
background during a refresh.
+ - Pre-processing Markdown now only happens when needed
- Make EmailsOnPushWorker use Sidekiq mailers queue
- Redesign all Devise emails. !4297
- Don't show 'Leave Project' to group members
- Fix wiki page events' webhook to point to the wiki repository
+ - Add a border around images to differentiate them from the background.
- Don't show tags for revert and cherry-pick operations
+ - Show image ID on registry page
- Fix issue todo not remove when leave project !4150 (Long Nguyen)
- Allow customisable text on the 'nearly there' page after a user signs up
- Bump recaptcha gem to 3.0.0 to remove deprecated stoken support
@@ -26,9 +33,11 @@ v 8.9.0 (unreleased)
- Added descriptions to notification settings dropdown
- Improve note validation to prevent errors when creating invalid note via API
- Reduce number of fog gem dependencies
+ - Add number of merge requests for a given milestone to the milestones view.
- Implement a fair usage of shared runners
- Remove project notification settings associated with deleted projects
- Fix 404 page when viewing TODOs that contain milestones or labels in different projects
+ - Wrap code blocks on Activies and Todos page !4783 (winniehell)
- Add a metric for the number of new Redis connections created by a transaction
- Fix Error 500 when viewing a blob with binary characters after the 1024-byte mark
- Redesign navigation for project pages
@@ -53,6 +62,7 @@ v 8.9.0 (unreleased)
- Add `sha` parameter to MR merge API, to ensure only reviewed changes are merged
- Don't allow MRs to be merged when commits were added since the last review / page load
- Add DB index on users.state
+ - Limit email on push diff size to 30 files / 150 KB
- Add rake task 'gitlab:db:configure' for conditionally seeding or migrating the database
- Changed the Slack build message to use the singular duration if necessary (Aran Koning)
- Fix race condition on merge when build succeeds
@@ -67,6 +77,7 @@ v 8.9.0 (unreleased)
- Todos will display target state if issuable target is 'Closed' or 'Merged'
- Validate only and except regexp
- Fix bug when sorting issues by milestone due date and filtering by two or more labels
+ - POST to API /projects/:id/runners/:runner_id would give 409 if the runner was already enabled for this project
- Add support for using Yubikeys (U2F) for two-factor authentication
- Link to blank group icon doesn't throw a 404 anymore
- Remove 'main language' feature
@@ -74,12 +85,16 @@ v 8.9.0 (unreleased)
- Pipelines can be canceled only when there are running builds
- Allow authentication using personal access tokens
- Use downcased path to container repository as this is expected path by Docker
+ - Allow to use CI token to fetch LFS objects
+ - Custom notification settings
- Projects pending deletion will render a 404 page
- Measure queue duration between gitlab-workhorse and Rails
- Added Gfm autocomplete for labels
+ - Added edit note 'up' shortcut documentation to the help panel and docs screenshot #18114
- Make Omniauth providers specs to not modify global configuration
- Remove unused JiraIssue class and replace references with ExternalIssue. !4659 (Ilan Shamir)
- Make authentication service for Container Registry to be compatible with < Docker 1.11
+ - Make it possible to lock a runner from being enabled for other projects
- Add Application Setting to configure Container Registry token expire delay (default 5min)
- Cache assigned issue and merge request counts in sidebar nav
- Use Knapsack only in CI environment
@@ -97,6 +112,7 @@ v 8.9.0 (unreleased)
- An indicator is now displayed at the top of the comment field for confidential issues.
- Show categorised search queries in the search autocomplete
- RepositoryCheck::SingleRepositoryWorker public and private methods are now instrumented
+ - Dropdown for `.gitlab-ci.yml` templates
- Improve issuables APIs performance when accessing notes !4471
- Add sorting dropdown to tags page !4423
- External links now open in a new tab
@@ -125,10 +141,17 @@ v 8.9.0 (unreleased)
- Various associations are now eager loaded when parsing issue references to reduce the number of queries executed
- Set inverse_of for Project/Service association to reduce the number of queries
- Update tanuki logo highlight/loading colors
+ - Remove explicit Gitlab::Metrics.action assignments, are already automatic.
- Use Git cached counters for branches and tags on project page
+ - Cache participable participants in an instance variable.
- Filter parameters for request_uri value on instrumented transactions.
+ - Remove duplicated keys add UNIQUE index to keys fingerprint column
+ - ExtractsPath get ref_names from repository cache, if not there access git.
- Cache user todo counts from TodoService
- Ensure Todos counters doesn't count Todos for projects pending delete
+ - Add left/right arrows horizontal navigation
+ - Add tooltip to pin/unpin navbar
+ - Add new sub nav style to Wiki and Graphs sub navigation
v 8.8.5
- Import GitHub repositories respecting the API rate limit !4166
diff --git a/Gemfile b/Gemfile
index bc1223e1bbc..092ea9d69b0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -48,7 +48,7 @@ gem 'attr_encrypted', '~> 3.0.0'
gem 'u2f', '~> 0.2.1'
# Browser detection
-gem "browser", '~> 2.0.3'
+gem "browser", '~> 2.2'
# Extracting information from a git repository
# Provide access to Gitlab::Git library
@@ -330,7 +330,7 @@ gem "newrelic_rpm", '~> 3.14'
gem 'octokit', '~> 4.3.0'
-gem "mail_room", "~> 0.7"
+gem "mail_room", "~> 0.8"
gem 'email_reply_parser', '~> 0.5.8'
diff --git a/Gemfile.lock b/Gemfile.lock
index 49e548fb94f..ba16e4bf337 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -98,7 +98,7 @@ GEM
autoprefixer-rails (>= 5.2.1)
sass (>= 3.3.4)
brakeman (3.3.2)
- browser (2.0.3)
+ browser (2.2.0)
builder (3.2.2)
bullet (5.0.0)
activesupport (>= 3.0.0)
@@ -398,7 +398,7 @@ GEM
systemu (~> 2.6.2)
mail (2.6.4)
mime-types (>= 1.16, < 4)
- mail_room (0.7.0)
+ mail_room (0.8.0)
method_source (0.8.2)
mime-types (2.99.2)
mimemagic (0.3.0)
@@ -833,7 +833,7 @@ DEPENDENCIES
binding_of_caller (~> 0.7.2)
bootstrap-sass (~> 3.3.0)
brakeman (~> 3.3.0)
- browser (~> 2.0.3)
+ browser (~> 2.2)
bullet
bundler-audit
byebug
@@ -899,7 +899,7 @@ DEPENDENCIES
license_finder
licensee (~> 8.0.0)
loofah (~> 2.0.3)
- mail_room (~> 0.7)
+ mail_room (~> 0.8)
method_source (~> 0.8)
minitest (~> 5.7.0)
mousetrap-rails (~> 1.4.6)
diff --git a/app/assets/javascripts/LabelManager.js.coffee b/app/assets/javascripts/LabelManager.js.coffee
index b06bcf0fcbf..6d8faba40d7 100644
--- a/app/assets/javascripts/LabelManager.js.coffee
+++ b/app/assets/javascripts/LabelManager.js.coffee
@@ -27,6 +27,11 @@ class @LabelManager
$btn = $(e.currentTarget)
$label = $("##{$btn.data('domId')}")
action = if $btn.parents('.js-prioritized-labels').length then 'remove' else 'add'
+
+ # Make sure tooltip will hide
+ $tooltip = $ "##{$btn.find('.has-tooltip:visible').attr('aria-describedby')}"
+ $tooltip.tooltip 'destroy'
+
_this.toggleLabelPriority($label, action)
toggleLabelPriority: ($label, action, persistState = true) ->
diff --git a/app/assets/javascripts/api.js.coffee b/app/assets/javascripts/api.js.coffee
index 3f61ea1eaf4..cf46f15a156 100644
--- a/app/assets/javascripts/api.js.coffee
+++ b/app/assets/javascripts/api.js.coffee
@@ -7,6 +7,7 @@
labelsPath: "/api/:version/projects/:id/labels"
licensePath: "/api/:version/licenses/:key"
gitignorePath: "/api/:version/gitignores/:key"
+ gitlabCiYmlPath: "/api/:version/gitlab_ci_ymls/:key"
group: (group_id, callback) ->
url = Api.buildUrl(Api.groupPath)
@@ -110,6 +111,12 @@
$.get url, (gitignore) ->
callback(gitignore)
+ gitlabCiYml: (key, callback) ->
+ url = Api.buildUrl(Api.gitlabCiYmlPath).replace(':key', key)
+
+ $.get url, (file) ->
+ callback(file)
+
buildUrl: (url) ->
url = gon.relative_url_root + url if gon.relative_url_root?
return url.replace(':version', gon.api_version)
diff --git a/app/assets/javascripts/application.js.coffee b/app/assets/javascripts/application.js.coffee
index 2f9f6c3ef5b..0206db461da 100644
--- a/app/assets/javascripts/application.js.coffee
+++ b/app/assets/javascripts/application.js.coffee
@@ -121,6 +121,11 @@ window.onload = ->
setTimeout shiftWindow, 100
$ ->
+
+ $document = $(document)
+ $window = $(window)
+ $body = $('body')
+
gl.utils.preventDisabledButtons()
bootstrapBreakpoint = bp.getBreakpointSize()
@@ -152,7 +157,7 @@ $ ->
), 1
# Initialize tooltips
- $('body').tooltip(
+ $body.tooltip(
selector: '.has-tooltip, [data-toggle="tooltip"]'
placement: (_, el) ->
$el = $(el)
@@ -171,7 +176,7 @@ $ ->
flash.show()
# Disable form buttons while a form is submitting
- $('body').on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
+ $body.on 'ajax:complete, ajax:beforeSend, submit', 'form', (e) ->
buttons = $('[type="submit"]', @)
switch e.type
@@ -184,7 +189,7 @@ $ ->
$('.account-box').hover -> $(@).toggleClass('hover')
# Commit show suppressed diff
- $(document).on 'click', '.diff-content .js-show-suppressed-diff', ->
+ $document.on 'click', '.diff-content .js-show-suppressed-diff', ->
$container = $(@).parent()
$container.next('table').show()
$container.remove()
@@ -197,13 +202,13 @@ $ ->
$('.navbar-toggle i').toggleClass("fa-angle-right fa-angle-left")
# Show/hide comments on diff
- $("body").on "click", ".js-toggle-diff-comments", (e) ->
+ $body.on "click", ".js-toggle-diff-comments", (e) ->
$(@).toggleClass('active')
$(@).closest(".diff-file").find(".notes_holder").toggle()
e.preventDefault()
- $(document).off "click", '.js-confirm-danger'
- $(document).on "click", '.js-confirm-danger', (e) ->
+ $document.off "click", '.js-confirm-danger'
+ $document.on "click", '.js-confirm-danger', (e) ->
e.preventDefault()
btn = $(e.target)
text = btn.data("confirm-danger-message")
@@ -211,7 +216,7 @@ $ ->
new ConfirmDangerModal(form, text)
- $(document).on 'click', 'button', ->
+ $document.on 'click', 'button', ->
$(this).blur()
$('input[type="search"]').each ->
@@ -219,7 +224,7 @@ $ ->
$this.attr 'value', $this.val()
return
- $(document)
+ $document
.off 'keyup', 'input[type="search"]'
.on 'keyup', 'input[type="search"]' , (e) ->
$this = $(this)
@@ -227,7 +232,7 @@ $ ->
$sidebarGutterToggle = $('.js-sidebar-toggle')
- $(document)
+ $document
.off 'breakpoint:change'
.on 'breakpoint:change', (e, breakpoint) ->
if breakpoint is 'sm' or breakpoint is 'xs'
@@ -239,14 +244,14 @@ $ ->
oldBootstrapBreakpoint = bootstrapBreakpoint
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint != oldBootstrapBreakpoint
- $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+ $document.trigger('breakpoint:change', [bootstrapBreakpoint])
checkInitialSidebarSize = ->
bootstrapBreakpoint = bp.getBreakpointSize()
if bootstrapBreakpoint is "xs" or "sm"
- $(document).trigger('breakpoint:change', [bootstrapBreakpoint])
+ $document.trigger('breakpoint:change', [bootstrapBreakpoint])
- $(window)
+ $window
.off "resize.app"
.on "resize.app", (e) ->
fitSidebarForSize()
@@ -256,29 +261,45 @@ $ ->
new Aside()
# Sidenav pinning
- if $(window).width() < 1440 and $.cookie('pin_nav') is 'true'
- $.cookie('pin_nav', 'false')
+ if $window.width() < 1440 and $.cookie('pin_nav') is 'true'
+ $.cookie('pin_nav', 'false', { path: '/' })
$('.page-with-sidebar')
.toggleClass('page-sidebar-collapsed page-sidebar-expanded')
.removeClass('page-sidebar-pinned')
$('.navbar-fixed-top').removeClass('header-pinned-nav')
- $(document)
+ $document
.off 'click', '.js-nav-pin'
.on 'click', '.js-nav-pin', (e) ->
e.preventDefault()
+ $pinBtn = $(e.currentTarget)
+ $page = $ '.page-with-sidebar'
+ $topNav = $ '.navbar-fixed-top'
+ $tooltip = $ "##{$pinBtn.attr('aria-describedby')}"
+ doPinNav = not $page.is('.page-sidebar-pinned')
+ tooltipText = 'Pin navigation'
+
$(this).toggleClass 'is-active'
- if $.cookie('pin_nav') is 'true'
- $.cookie 'pin_nav', 'false'
- $('.page-with-sidebar')
- .removeClass('page-sidebar-pinned')
- .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
- $('.navbar-fixed-top')
- .removeClass('header-pinned-nav')
- .toggleClass('header-collapsed header-expanded')
+ if doPinNav
+ $page.addClass('page-sidebar-pinned')
+ $topNav.addClass('header-pinned-nav')
else
- $.cookie 'pin_nav', 'true'
- $('.page-with-sidebar').addClass('page-sidebar-pinned')
- $('.navbar-fixed-top').addClass('header-pinned-nav')
+ $tooltip.remove() # Remove it immediately when collapsing the sidebar
+ $page.removeClass('page-sidebar-pinned')
+ .toggleClass('page-sidebar-collapsed page-sidebar-expanded')
+ $topNav.removeClass('header-pinned-nav')
+ .toggleClass('header-collapsed header-expanded')
+
+ # Save settings
+ $.cookie 'pin_nav', doPinNav, { path: '/' }
+
+ if $.cookie('pin_nav') is 'true' or doPinNav
+ tooltipText = 'Unpin navigation'
+
+ # Update tooltip text immediately
+ $tooltip.find('.tooltip-inner').text(tooltipText)
+
+ # Persist tooltip title
+ $pinBtn.attr('title', tooltipText).tooltip('fixTitle')
diff --git a/app/assets/javascripts/blob/blob_ci_yaml.js.coffee b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
new file mode 100644
index 00000000000..d9a03d05529
--- /dev/null
+++ b/app/assets/javascripts/blob/blob_ci_yaml.js.coffee
@@ -0,0 +1,23 @@
+#= require blob/template_selector
+
+class @BlobCiYamlSelector extends TemplateSelector
+ requestFile: (query) ->
+ Api.gitlabCiYml query.name, @requestFileSuccess.bind(@)
+
+class @BlobCiYamlSelectors
+ constructor: (opts) ->
+ {
+ @$dropdowns = $('.js-gitlab-ci-yml-selector')
+ @editor
+ } = opts
+
+ @$dropdowns.each (i, dropdown) =>
+ $dropdown = $(dropdown)
+
+ new BlobCiYamlSelector(
+ pattern: /(.gitlab-ci.yml)/,
+ data: $dropdown.data('data'),
+ wrapper: $dropdown.closest('.js-gitlab-ci-yml-selector-wrap'),
+ dropdown: $dropdown,
+ editor: @editor
+ )
diff --git a/app/assets/javascripts/blob/edit_blob.js.coffee b/app/assets/javascripts/blob/edit_blob.js.coffee
index 636f909dbd0..19e584519d7 100644
--- a/app/assets/javascripts/blob/edit_blob.js.coffee
+++ b/app/assets/javascripts/blob/edit_blob.js.coffee
@@ -15,6 +15,7 @@ class @EditBlob
new BlobLicenseSelectors { @editor }
new BlobGitignoreSelectors { @editor }
+ new BlobCiYamlSelectors { @editor }
initModePanesAndLinks: ->
@$editModePanes = $(".js-edit-mode-pane")
diff --git a/app/assets/javascripts/dispatcher.js.coffee b/app/assets/javascripts/dispatcher.js.coffee
index b560500cce6..7fbff9214cf 100644
--- a/app/assets/javascripts/dispatcher.js.coffee
+++ b/app/assets/javascripts/dispatcher.js.coffee
@@ -78,6 +78,7 @@ class Dispatcher
when 'projects:show'
shortcut_handler = new ShortcutsNavigation()
+ new NotificationsForm()
new TreeView() if $('#tree-slider').length
when 'groups:activity'
new Activities()
@@ -129,6 +130,8 @@ class Dispatcher
shortcut_handler = new ShortcutsDashboardNavigation()
when 'profiles'
new Profile()
+ new NotificationsForm()
+ new NotificationsDropdown()
when 'projects'
new Project()
new ProjectAvatar()
@@ -136,8 +139,12 @@ class Dispatcher
when 'edit'
shortcut_handler = new ShortcutsNavigation()
new ProjectNew()
- when 'new', 'show'
+ when 'new'
new ProjectNew()
+ when 'show'
+ new ProjectNew()
+ new ProjectShow()
+ new NotificationsDropdown()
when 'wikis'
new Wikis()
shortcut_handler = new ShortcutsNavigation()
diff --git a/app/assets/javascripts/gl_dropdown.js.coffee b/app/assets/javascripts/gl_dropdown.js.coffee
index b49bd4565a7..2a7bf0bc306 100644
--- a/app/assets/javascripts/gl_dropdown.js.coffee
+++ b/app/assets/javascripts/gl_dropdown.js.coffee
@@ -58,7 +58,7 @@ class GitLabDropdownFilter
filter: (search_text) ->
data = @options.data()
- if data?
+ if data? and not @options.filterByText
results = data
if search_text isnt ''
@@ -102,10 +102,11 @@ class GitLabDropdownFilter
$el = $(@)
matches = fuzzaldrinPlus.match($el.text().trim(), search_text)
- if matches.length
- $el.show()
- else
- $el.hide()
+ unless $el.is('.dropdown-header')
+ if matches.length
+ $el.show()
+ else
+ $el.hide()
else
elements.show()
@@ -191,6 +192,7 @@ class GitLabDropdown
if @options.filterable
@filter = new GitLabDropdownFilter @filterInput,
filterInputBlur: @filterInputBlur
+ filterByText: @options.filterByText
remote: @options.filterRemote
query: @options.data
keys: searchFields
@@ -302,6 +304,9 @@ class GitLabDropdown
if @options.setIndeterminateIds
@options.setIndeterminateIds.call(@)
+ if @options.setActiveIds
+ @options.setActiveIds.call(@)
+
# Makes indeterminate items effective
if @fullData and @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@parseData @fullData
diff --git a/app/assets/javascripts/gl_form.js.coffee b/app/assets/javascripts/gl_form.js.coffee
index d540cc4dc46..77512d187c9 100644
--- a/app/assets/javascripts/gl_form.js.coffee
+++ b/app/assets/javascripts/gl_form.js.coffee
@@ -34,6 +34,8 @@ class @GLForm
# form and textarea event listeners
@addEventListeners()
+ gl.text.init(@form)
+
# hide discard button
@form.find('.js-note-discard').hide()
@@ -42,6 +44,7 @@ class @GLForm
clearEventListeners: ->
@textarea.off 'focus'
@textarea.off 'blur'
+ gl.text.removeListeners(@form)
addEventListeners: ->
@textarea.on 'focus', ->
diff --git a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
index 584d281a510..834a81af459 100644
--- a/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
+++ b/app/assets/javascripts/graphs/stat_graph_contributors_graph.js.coffee
@@ -121,7 +121,11 @@ class @ContributorsMasterGraph extends ContributorsGraph
class @ContributorsAuthorGraph extends ContributorsGraph
constructor: (@data) ->
- @width = $('.content').width()/2 - 100
+ # Don't split graph size in half for mobile devices.
+ if $(window).width() < 768
+ @width = $('.content').width() - 80
+ else
+ @width = ($('.content').width() / 2) - 100
@height = 200
@x = null
@y = null
diff --git a/app/assets/javascripts/labels_select.js.coffee b/app/assets/javascripts/labels_select.js.coffee
index d350a7c0e7f..6a10db10eb1 100644
--- a/app/assets/javascripts/labels_select.js.coffee
+++ b/app/assets/javascripts/labels_select.js.coffee
@@ -210,9 +210,21 @@ class @LabelsSelect
if $dropdown.hasClass('js-filter-bulk-update')
indeterminate = instance.indeterminateIds
+ active = instance.activeIds
+
if indeterminate.indexOf(label.id) isnt -1
selectedClass.push 'is-indeterminate'
+ if active.indexOf(label.id) isnt -1
+ # Remove is-indeterminate class if the item will be marked as active
+ i = selectedClass.indexOf 'is-indeterminate'
+ selectedClass.splice i, 1 unless i is -1
+
+ selectedClass.push 'is-active'
+
+ # Add input manually
+ instance.addInput @fieldName, label.id
+
if $form.find("input[type='hidden']\
[name='#{$dropdown.data('fieldName')}']\
[value='#{this.id(label)}']").length
@@ -328,6 +340,10 @@ class @LabelsSelect
setIndeterminateIds: ->
if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
@indeterminateIds = _this.getIndeterminateIds()
+
+ setActiveIds: ->
+ if @dropdown.find('.dropdown-menu-toggle').hasClass('js-filter-bulk-update')
+ @activeIds = _this.getActiveIds()
)
@bindEvents()
@@ -352,3 +368,12 @@ class @LabelsSelect
label_ids.push $("#issue_#{issue_id}").data('labels')
_.flatten(label_ids)
+
+ getActiveIds: ->
+ label_ids = []
+
+ $('.selected_issue:checked').each (i, el) ->
+ issue_id = $(el).data('id')
+ label_ids.push $("#issue_#{issue_id}").data('labels')
+
+ _.intersection.apply _, label_ids
diff --git a/app/assets/javascripts/lib/text_utility.js.coffee b/app/assets/javascripts/lib/text_utility.js.coffee
new file mode 100644
index 00000000000..bb2772dfed2
--- /dev/null
+++ b/app/assets/javascripts/lib/text_utility.js.coffee
@@ -0,0 +1,79 @@
+((w) ->
+ w.gl ?= {}
+ w.gl.text ?= {}
+
+ gl.text.randomString = -> Math.random().toString(36).substring(7)
+
+ gl.text.replaceRange = (s, start, end, substitute) ->
+ s.substring(0, start) + substitute + s.substring(end);
+
+ gl.text.selectedText = (text, textarea) ->
+ text.substring(textarea.selectionStart, textarea.selectionEnd)
+
+ gl.text.insertText = (textArea, text, tag, selected, wrap) ->
+ selectedSplit = selected.split('\n')
+ startChar = if not wrap and textArea.selectionStart > 0 then '\n' else ''
+
+ if selectedSplit.length > 1 and not wrap
+ insertText = selectedSplit.map((val) ->
+ if val.indexOf(tag) is 0
+ "#{val.replace(tag, '')}"
+ else
+ "#{tag}#{val}"
+ ).join('\n')
+ else
+ insertText = "#{startChar}#{tag}#{selected}#{if wrap then tag else ' '}"
+
+ if document.queryCommandSupported('insertText')
+ document.execCommand 'insertText', false, insertText
+ else
+ try
+ document.execCommand("ms-beginUndoUnit")
+
+ textArea.value = @replaceRange(
+ text,
+ textArea.selectionStart,
+ textArea.selectionEnd,
+ insertText)
+ try
+ document.execCommand("ms-endUndoUnit")
+
+ @moveCursor(textArea, tag, wrap)
+
+ gl.text.moveCursor = (textArea, tag, wrapped) ->
+ return unless textArea.setSelectionRange
+
+ if textArea.selectionStart is textArea.selectionEnd
+ if wrapped
+ pos = textArea.selectionStart - tag.length
+ else
+ pos = textArea.selectionStart
+
+ textArea.setSelectionRange pos, pos
+
+ gl.text.updateText = (textArea, tag, wrap) ->
+ $textArea = $(textArea)
+ oldVal = $textArea.val()
+ textArea = $textArea.get(0)
+ text = $textArea.val()
+ selected = @selectedText(text, textArea)
+ $textArea.focus()
+
+ @insertText(textArea, text, tag, selected, wrap)
+
+ gl.text.init = (form) ->
+ self = @
+ $('.js-md', form)
+ .off 'click'
+ .on 'click', ->
+ $this = $(@)
+ self.updateText(
+ $this.closest('.md-area').find('textarea'),
+ $this.data('md-tag'),
+ not $this.data('md-prepend')
+ )
+
+ gl.text.removeListeners = (form) ->
+ $('.js-md', form).off()
+
+) window
diff --git a/app/assets/javascripts/notes.js.coffee b/app/assets/javascripts/notes.js.coffee
index e2d3241437b..17f7e180127 100644
--- a/app/assets/javascripts/notes.js.coffee
+++ b/app/assets/javascripts/notes.js.coffee
@@ -102,12 +102,15 @@ class @Notes
keydownNoteText: (e) ->
$this = $(this)
- if $this.val() is '' and e.which is 38 #aka the up key
+ if $this.val() is '' and e.which is 38 and not isMetaKey e
myLastNote = $("li.note[data-author-id='#{gon.current_user_id}'][data-editable]:last")
if myLastNote.length
myLastNoteEditBtn = myLastNote.find('.js-note-edit')
myLastNoteEditBtn.trigger('click', [true, myLastNote])
+ isMetaKey = (e) ->
+ (e.metaKey or e.ctrlKey or e.altKey or e.shiftKey)
+
initRefresh: ->
clearInterval(Notes.interval)
Notes.interval = setInterval =>
diff --git a/app/assets/javascripts/notifications_dropdown.js.coffee b/app/assets/javascripts/notifications_dropdown.js.coffee
new file mode 100644
index 00000000000..0bbd082c156
--- /dev/null
+++ b/app/assets/javascripts/notifications_dropdown.js.coffee
@@ -0,0 +1,25 @@
+class @NotificationsDropdown
+ constructor: ->
+ $(document)
+ .off 'click', '.update-notification'
+ .on 'click', '.update-notification', (e) ->
+ e.preventDefault()
+
+ return if $(this).is('.is-active') and $(this).data('notification-level') is 'custom'
+
+ notificationLevel = $(@).data 'notification-level'
+ label = $(@).data 'notification-title'
+ form = $(this).parents('.notification-form:first')
+ form.find('.js-notification-loading').toggleClass 'fa-bell fa-spin fa-spinner'
+ form.find('#notification_setting_level').val(notificationLevel)
+ form.submit()
+
+ $(document)
+ .off 'ajax:success', '.notification-form'
+ .on 'ajax:success', '.notification-form', (e, data) ->
+ if data.saved
+ $(e.currentTarget)
+ .closest('.notification-dropdown')
+ .replaceWith(data.html)
+ else
+ new Flash('Failed to save new settings', 'alert')
diff --git a/app/assets/javascripts/notifications_form.js.coffee b/app/assets/javascripts/notifications_form.js.coffee
new file mode 100644
index 00000000000..3432428702a
--- /dev/null
+++ b/app/assets/javascripts/notifications_form.js.coffee
@@ -0,0 +1,49 @@
+class @NotificationsForm
+ constructor: ->
+ @removeEventListeners()
+ @initEventListeners()
+
+ removeEventListeners: ->
+ $(document).off 'change', '.js-custom-notification-event'
+
+ initEventListeners: ->
+ $(document).on 'change', '.js-custom-notification-event', @toggleCheckbox
+
+ toggleCheckbox: (e) =>
+ $checkbox = $(e.currentTarget)
+ $parent = $checkbox.closest('.checkbox')
+ @saveEvent($checkbox, $parent)
+
+ showCheckboxLoadingSpinner: ($parent) ->
+ $parent
+ .addClass 'is-loading'
+ .find '.custom-notification-event-loading'
+ .removeClass 'fa-check'
+ .addClass 'fa-spin fa-spinner'
+ .removeClass 'is-done'
+
+ saveEvent: ($checkbox, $parent) ->
+ form = $parent.parents('form:first')
+
+ $.ajax(
+ url: form.attr('action')
+ method: form.attr('method')
+ dataType: 'json'
+ data: form.serialize()
+
+ beforeSend: =>
+ @showCheckboxLoadingSpinner($parent)
+ ).done (data) ->
+ $checkbox.enable()
+
+ if data.saved
+ $parent
+ .find '.custom-notification-event-loading'
+ .toggleClass 'fa-spin fa-spinner fa-check is-done'
+
+ setTimeout(->
+ $parent
+ .removeClass 'is-loading'
+ .find '.custom-notification-event-loading'
+ .toggleClass 'fa-spin fa-spinner fa-check is-done'
+ , 2000)
diff --git a/app/assets/javascripts/profile.js.coffee b/app/assets/javascripts/profile.js.coffee
index 26a12423521..1583d1ba6f9 100644
--- a/app/assets/javascripts/profile.js.coffee
+++ b/app/assets/javascripts/profile.js.coffee
@@ -8,6 +8,10 @@ class @Profile
$('.js-preferences-form').on 'change.preference', 'input[type=radio]', ->
$(this).parents('form').submit()
+ # Automatically submit email form when it changes
+ $('#user_notification_email').on 'change', ->
+ $(this).parents('form').submit()
+
$('.update-username').on 'ajax:before', ->
$('.loading-username').show()
$(this).find('.update-success').hide()
diff --git a/app/assets/javascripts/project.js.coffee b/app/assets/javascripts/project.js.coffee
index 07be85a32a5..96e10dd7e8a 100644
--- a/app/assets/javascripts/project.js.coffee
+++ b/app/assets/javascripts/project.js.coffee
@@ -19,6 +19,7 @@ class @Project
$('.clone').text(url)
# Ref switcher
+ @initRefSwitcher()
$('.project-refs-select').on 'change', ->
$(@).parents('form').submit()
@@ -34,23 +35,6 @@ class @Project
$(@).parents('.no-password-message').remove()
e.preventDefault()
- $('.update-notification').on 'click', (e) ->
- e.preventDefault()
- notification_level = $(@).data 'notification-level'
- label = $(@).data 'notification-title'
- $('#notification_setting_level').val(notification_level)
- $('#notification-form').submit()
- $('#notifications-button').empty().append("<i class='fa fa-bell'></i>" + label + "<i class='fa fa-angle-down'></i>")
- $(@).parents('ul').find('li.active').removeClass 'active'
- $(@).parent().addClass 'active'
-
- $('#notification-form').on 'ajax:success', (e, data) ->
- if data.saved
- new Flash("Notification settings saved", "notice")
- else
- new Flash("Failed to save new settings", "alert")
-
-
@projectSelectDropdown()
projectSelectDropdown: ->
@@ -66,3 +50,39 @@ class @Project
changeProject: (url) ->
window.location = url
+
+ initRefSwitcher: ->
+ $('.js-project-refs-dropdown').each ->
+ $dropdown = $(@)
+ selected = $dropdown.data('selected')
+
+ $dropdown.glDropdown(
+ data: (term, callback) ->
+ $.ajax(
+ url: $dropdown.data('refs-url')
+ data:
+ ref: $dropdown.data('ref')
+ ).done (refs) ->
+ callback(refs)
+ selectable: true
+ filterable: true
+ filterByText: true
+ fieldName: 'ref'
+ renderRow: (ref) ->
+ if ref.header?
+ "<li class='dropdown-header'>#{ref.header}</li>"
+ else
+ isActiveClass = if ref is selected then 'is-active' else ''
+
+ "<li>
+ <a href='#' data-ref='#{escape(ref)}' class='#{isActiveClass}'>
+ #{ref}
+ </a>
+ </li>"
+ id: (obj, $el) ->
+ $el.data('ref')
+ toggleLabel: (obj, $el) ->
+ $el.text().trim()
+ clicked: (e) ->
+ $dropdown.closest('form').submit()
+ )
diff --git a/app/assets/javascripts/right_sidebar.js.coffee b/app/assets/javascripts/right_sidebar.js.coffee
index 8eb005b0a22..12340bbce54 100644
--- a/app/assets/javascripts/right_sidebar.js.coffee
+++ b/app/assets/javascripts/right_sidebar.js.coffee
@@ -51,15 +51,19 @@ class @Sidebar
$this = $(e.currentTarget)
$todoLoading = $('.js-issuable-todo-loading')
$btnText = $('.js-issuable-todo-text', $this)
- ajaxType = if $this.attr('data-id') then 'PATCH' else 'POST'
- ajaxUrlExtra = if $this.attr('data-id') then "/#{$this.attr('data-id')}" else ''
+ ajaxType = if $this.attr('data-delete-path') then 'DELETE' else 'POST'
+
+ if $this.attr('data-delete-path')
+ url = "#{$this.attr('data-delete-path')}"
+ else
+ url = "#{$this.data('url')}"
$.ajax(
- url: "#{$this.data('url')}#{ajaxUrlExtra}"
+ url: url
type: ajaxType
dataType: 'json'
data:
- issuable_id: $this.data('issuable')
+ issuable_id: $this.data('issuable-id')
issuable_type: $this.data('issuable-type')
beforeSend: =>
@beforeTodoSend($this, $todoLoading)
@@ -82,15 +86,15 @@ class @Sidebar
else
$todoPendingCount.removeClass 'hidden'
- if data.todo?
+ if data.delete_path?
$btn
.attr 'aria-label', $btn.data('mark-text')
- .attr 'data-id', data.todo.id
+ .attr 'data-delete-path', data.delete_path
$btnText.text $btn.data('mark-text')
else
$btn
.attr 'aria-label', $btn.data('todo-text')
- .removeAttr 'data-id'
+ .removeAttr 'data-delete-path'
$btnText.text $btn.data('todo-text')
sidebarDropdownLoading: (e) ->
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 3cbddc59f11..a306b8f3f29 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -37,3 +37,4 @@
@import "framework/timeline.scss";
@import "framework/typography.scss";
@import "framework/zen.scss";
+@import "framework/blank";
diff --git a/app/assets/stylesheets/framework/blank.scss b/app/assets/stylesheets/framework/blank.scss
new file mode 100644
index 00000000000..40b5171a8c6
--- /dev/null
+++ b/app/assets/stylesheets/framework/blank.scss
@@ -0,0 +1,23 @@
+.blank-state {
+ padding-top: 20px;
+ padding-bottom: 20px;
+ text-align: center;
+}
+
+.blank-state-no-icon {
+ padding-top: 40px;
+ padding-bottom: 40px;
+}
+
+.blank-state-title {
+ margin-top: 0;
+ margin-bottom: 5px;
+ font-size: 19px;
+ font-weight: normal;
+}
+
+.blank-state-text {
+ margin-top: 0;
+ margin-bottom: $gl-padding;
+ font-size: 15px;
+}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index d5fe5bc2ef1..38023818709 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -97,6 +97,22 @@
}
}
+.sub-header-block {
+ background-color: $white-light;
+ border-bottom: 1px solid $white-dark;
+ padding: 11px 0;
+ margin-bottom: 11px;
+
+ .oneline {
+ line-height: 35px;
+ }
+
+ &.no-bottom-space {
+ border-bottom: 0;
+ margin-bottom: 0;
+ }
+}
+
.cover-block {
text-align: center;
background: $background-color;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index d4d579a083d..00111dfa706 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -461,10 +461,12 @@
}
}
- .ui-state-active,
- .ui-state-hover {
- color: $md-link-color;
- background-color: $calendar-hover-bg;
+ .ui-datepicker-calendar {
+ .ui-state-hover,
+ .ui-state-active {
+ color: #fff;
+ border: 0;
+ }
}
.ui-datepicker-prev,
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index fd885b38680..fd8eaa8a691 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -65,6 +65,11 @@
a {
padding-top: 0;
line-height: 1;
+ border-bottom: 1px solid $border-color;
+
+ &.btn.btn-xs {
+ padding: 2px 5px;
+ }
}
}
}
@@ -97,5 +102,30 @@
white-space: pre-wrap;
word-break: keep-all;
}
+
+ @include bulleted-list;
+ }
+}
+
+.toolbar-group {
+ float: left;
+ margin-right: -5px;
+ margin-left: $gl-padding;
+
+ &:first-child {
+ margin-left: 0;
+ }
+}
+
+.toolbar-btn {
+ float: left;
+ padding: 0 5px;
+ color: #959494;
+ background: transparent;
+ border: 0;
+ outline: 0;
+
+ &:hover {
+ color: $gl-link-color;
}
}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 828e7224231..5ec5a96a597 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -110,3 +110,17 @@
font-size: 16px;
line-height: 24px;
}
+
+@mixin bulleted-list {
+ > ul {
+ list-style-type: disc;
+
+ ul {
+ list-style-type: circle;
+
+ ul {
+ list-style-type: square;
+ }
+ }
+ }
+} \ No newline at end of file
diff --git a/app/assets/stylesheets/framework/mobile.scss b/app/assets/stylesheets/framework/mobile.scss
index d4e5cc819a4..c74682dfef4 100644
--- a/app/assets/stylesheets/framework/mobile.scss
+++ b/app/assets/stylesheets/framework/mobile.scss
@@ -52,6 +52,19 @@
.git-clone-holder {
display: none;
}
+
+ // Display Star and Fork buttons without counters on mobile.
+ .project-action-buttons {
+ display: block;
+
+ .count-buttons .btn {
+ margin: 0 10px;
+ }
+
+ .count-buttons .count-with-arrow {
+ display: none;
+ }
+ }
}
.project-stats {
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index a55918f8711..0281b06d3ba 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -18,6 +18,13 @@
opacity: 0;
transition-duration: .3s;
}
+
+ .fa {
+ position: relative;
+ top: 3px;
+ font-size: 13px;
+ color: $btn-placeholder-gray;
+ }
}
@mixin scrolling-links() {
@@ -104,10 +111,6 @@
width: 50%;
line-height: 28px;
- &.wiki-page {
- padding: 16px 10px 11px;
- }
-
/* Small devices (phones, tablets, 768px and lower) */
@media (max-width: $screen-sm-min) {
width: 100%;
@@ -136,7 +139,7 @@
}
/* Small devices (phones, tablets, 768px and lower) */
- @media (max-width: $screen-sm-max) {
+ @media (max-width: $screen-xs-max) {
width: 100%;
}
}
@@ -220,6 +223,7 @@
form {
display: block;
height: auto;
+ margin-bottom: 14px;
input {
width: 100%;
@@ -319,11 +323,19 @@
.fade-right {
@include fade(left, rgba(250, 250, 250, 0.4), $background-color);
right: 0;
+
+ .fa {
+ right: -7px;
+ }
}
.fade-left {
@include fade(right, rgba(250, 250, 250, 0.4), $background-color);
left: 0;
+
+ .fa {
+ left: -7px;
+ }
}
li {
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index ae7bdf14c40..874416e1007 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -9,6 +9,10 @@
margin-top: -2px;
float: right;
}
+
+ .dropdown-menu-toggle {
+ line-height: 20px;
+ }
}
.panel-body {
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index f242706ebe4..21d87cc9d34 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -165,11 +165,6 @@
background-size: 16px 16px !important;
}
-/** Branch/tag selector **/
-.project-refs-form .select2-container {
- width: 160px !important;
-}
-
.select2-results .select2-no-results,
.select2-results .select2-searching,
.select2-results .select2-ajax-error,
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index a0bb3427af0..98f917ce69b 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -91,7 +91,6 @@
text-decoration: none;
font-weight: normal;
outline: none;
- white-space: nowrap;
&:hover,
&:active,
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 761e33f0df7..de534d28421 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -80,9 +80,14 @@
.commit {
padding: 10px 0;
+ position: relative;
@media (min-width: $screen-sm-min) {
- padding-left: 46px;
+ padding-left: 20px;
+
+ .commit-info-block {
+ padding-left: 44px;
+ }
}
&:not(:last-child) {
@@ -95,8 +100,11 @@
vertical-align: baseline;
}
+
.avatar {
- margin-left: -46px;
+ position: absolute;
+ top: 10px;
+ left: 16px;
}
.item-title {
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 1a7d5f9666e..5286b73cc50 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -4,6 +4,11 @@
margin-bottom: $gl-padding;
border-radius: 3px;
+ .commit-short-id {
+ font-family: $regular_font;
+ font-weight: 400;
+ }
+
.diff-header {
position: relative;
background: $background-color;
diff --git a/app/assets/stylesheets/pages/editor.scss b/app/assets/stylesheets/pages/editor.scss
index a34b06f1054..1aa4e06d975 100644
--- a/app/assets/stylesheets/pages/editor.scss
+++ b/app/assets/stylesheets/pages/editor.scss
@@ -60,13 +60,14 @@
.encoding-selector,
.license-selector,
- .gitignore-selector {
+ .gitignore-selector,
+ .gitlab-ci-yml-selector {
display: inline-block;
vertical-align: top;
font-family: $regular_font;
}
- .gitignore-selector, .license-selector {
+ .gitignore-selector, .license-selector, .gitlab-ci-yml-selector {
.dropdown {
line-height: 21px;
}
@@ -76,4 +77,10 @@
width: 220px;
}
}
+
+ .gitlab-ci-yml-selector {
+ .dropdown-menu-toggle {
+ width: 250px;
+ }
+ }
}
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index 6fe57c737b3..a2145956eb5 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -54,6 +54,10 @@
}
}
+ code {
+ white-space: pre-wrap;
+ }
+
pre {
border: none;
background: #f9f9f9;
@@ -136,9 +140,10 @@
.event-last-push {
overflow: auto;
width: 100%;
+
.event-last-push-text {
@include str-truncated(100%);
- padding: 5px 0;
+ padding: 4px 0;
font-size: 13px;
float: left;
margin-right: -150px;
diff --git a/app/assets/stylesheets/pages/help.scss b/app/assets/stylesheets/pages/help.scss
index 4a95b7b852e..0b710ef168b 100644
--- a/app/assets/stylesheets/pages/help.scss
+++ b/app/assets/stylesheets/pages/help.scss
@@ -57,4 +57,11 @@
.documentation {
padding: 7px;
+
+ // Border around images in the help pages.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px;
+ }
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 687117233f6..21ff6ab71f0 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -4,6 +4,13 @@
margin-right: 1px;
}
}
+
+ // Border around images in issue and MR descriptions.
+ .description img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px;
+ }
}
.issuable-filter-count {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index 046c38aba44..f5f67e2cd84 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -50,11 +50,10 @@
.label-row {
.label-name {
- display: block;
+ display: inline-block;
margin-bottom: 10px;
@media (min-width: $screen-sm-min) {
- display: inline-block;
width: 200px;
margin-bottom: 0;
}
@@ -63,6 +62,7 @@
.label-description {
display: block;
margin-bottom: 10px;
+ margin-left: 50px;
@media (min-width: $screen-sm-min) {
display: inline-block;
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index e67271adfb1..aca82f7f7bf 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -119,7 +119,12 @@
margin-bottom: 0;
}
- @media (max-width: $screen-sm-max) {
+ .btn-grouped {
+ margin-left: 0;
+ margin-right: 7px;
+ }
+
+ @media (max-width: $screen-xs-max) {
h4 {
font-size: 15px;
}
@@ -131,10 +136,14 @@
.btn,
.btn-group,
.accept-action {
- width: 100%;
margin-bottom: 4px;
}
+ .accept-action {
+ width: 100%;
+ text-align: center;
+ }
+
.accept-control {
width: 100%;
text-align: center;
@@ -284,7 +293,7 @@
margin-bottom: 0;
}
- @media (min-width: $screen-sm-min) {
+ @media (min-width: $screen-xs-min) {
float: left;
width: 50%;
margin-bottom: 0;
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 577dddae741..3784010348a 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -179,6 +179,10 @@
border-top: 1px solid $border-color;
}
+.md-helper {
+ padding-top: 10px;
+}
+
.toolbar-button {
padding: 0;
background: none;
@@ -219,3 +223,16 @@
float: left;
}
}
+
+.note-form-actions {
+ @media (max-width: $screen-xs-max) {
+ .btn {
+ float: none;
+ width: 100%;
+
+ &:not(:last-child) {
+ margin-bottom: 10px;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 35d728aec83..ffba3dc5bc6 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -84,24 +84,14 @@ ul.notes {
word-wrap: break-word;
@include md-typography;
+ // Reset ul style types since we're nested inside a ul already
+ @include bulleted-list;
+
// On diffs code should wrap nicely and not overflow
code {
white-space: pre-wrap;
}
- // Reset ul style types since we're nested inside a ul already
- & > ul {
- list-style-type: disc;
-
- ul {
- list-style-type: circle;
-
- ul {
- list-style-type: square;
- }
- }
- }
-
ul.task-list {
ul:not(.task-list) {
padding-left: 1.3em;
@@ -117,6 +107,13 @@ ul.notes {
code {
word-break: keep-all;
}
+
+ // Border around images in issue and MR comments.
+ img:not(.emoji) {
+ border: 1px solid $table-border-gray;
+ padding: 5px;
+ margin: 5px 0;
+ }
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 855d86cb238..d3e59d7fdb9 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -101,7 +101,8 @@
.notifications-btn {
- .fa-bell {
+ .fa-bell,
+ .fa-spinner {
margin-right: 6px;
}
@@ -128,11 +129,6 @@
}
}
- .btn-group:not(:first-child):not(:last-child) > .btn {
- border-top-right-radius: 3px;
- border-bottom-right-radius: 3px;
- }
-
form {
margin-left: 10px;
}
@@ -378,7 +374,7 @@ a.deploy-project-label {
.project-stats {
margin-top: $gl-padding;
margin-bottom: 0;
- padding: 16px 0;
+ padding: 0;
background-color: $white-light;
font-size: 0;
@@ -387,13 +383,14 @@ a.deploy-project-label {
}
.nav li {
- display: inline;
+ display: inline-block;
+ margin: 16px 0;
+ margin-right: 16px;
}
.nav > li > a {
background-color: transparent;
- margin-right: 12px;
- padding: 0 10px;
+ padding: 5px 10px;
font-size: 15px;
color: $notes-light-color;
}
@@ -407,12 +404,17 @@ a.deploy-project-label {
font-size: 17px;
}
- li.missing a {
- color: #5a6069;
- border: 1px dashed #dce0e5;
+ li.missing {
+ border: 1px dashed $border-gray-light;
+ border-radius: $border-radius-default;
+
+ a {
+ color: $notes-light-color;
+ display: block;
+ }
&:hover {
- background-color: #f0f2f5;
+ background-color: $gray-normal;
}
}
@@ -499,7 +501,8 @@ pre.light-well {
.activity-filter-block {
.controls {
- padding-bottom: 10px;
+ padding-bottom: 7px;
+ margin-top: 8px;
border-bottom: 1px solid $border-color;
}
}
@@ -603,3 +606,26 @@ pre.light-well {
}
}
}
+
+.custom-notifications-form {
+ .is-loading {
+ .custom-notification-event-loading {
+ display: inline-block;
+ }
+ }
+}
+
+.custom-notification-event-loading {
+ display: none;
+ margin-left: 5px;
+
+ &.is-done {
+ color: $gl-text-green;
+ }
+}
+
+.project-refs-form {
+ .dropdown-menu {
+ width: 300px;
+ }
+}
diff --git a/app/assets/stylesheets/pages/stat_graph.scss b/app/assets/stylesheets/pages/stat_graph.scss
index 85a0304196c..69288b31cc4 100644
--- a/app/assets/stylesheets/pages/stat_graph.scss
+++ b/app/assets/stylesheets/pages/stat_graph.scss
@@ -14,24 +14,38 @@
font-size: 10px;
}
+#contributors-master {
+ @include make-md-column(12);
+
+ svg {
+ width: 100%;
+ }
+}
+
#contributors {
.contributors-list {
margin: 0 0 10px;
list-style: none;
padding: 0;
+
+ svg {
+ width: 100%;
+ }
}
.person {
- &:nth-child(even) {
- float: right;
- }
- float: left;
+ @include make-md-column(6);
margin-top: 10px;
+
+ @media (max-width: $screen-sm-min) {
+ width: 100%;
+ }
}
.person .spark {
display: block;
background: #f3f3f3;
+ width: 100%;
}
.person .area-contributor {
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index afc00a68572..cf16d070cfe 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -62,6 +62,10 @@
}
}
+ code {
+ white-space: pre-wrap;
+ }
+
pre {
border: none;
background: #f9f9f9;
diff --git a/app/controllers/admin/appearances_controller.rb b/app/controllers/admin/appearances_controller.rb
index 26cf74e4849..4b0ec54b3f4 100644
--- a/app/controllers/admin/appearances_controller.rb
+++ b/app/controllers/admin/appearances_controller.rb
@@ -5,6 +5,7 @@ class Admin::AppearancesController < Admin::ApplicationController
end
def preview
+ render 'preview', layout: 'devise'
end
def create
diff --git a/app/controllers/admin/runner_projects_controller.rb b/app/controllers/admin/runner_projects_controller.rb
index d25619d94e0..bf20c5305a7 100644
--- a/app/controllers/admin/runner_projects_controller.rb
+++ b/app/controllers/admin/runner_projects_controller.rb
@@ -1,15 +1,14 @@
class Admin::RunnerProjectsController < Admin::ApplicationController
before_action :project, only: [:create]
- def index
- @runner_projects = project.runner_projects.all
- @runner_project = project.runner_projects.new
- end
-
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
- if @runner.assign_to(@project, current_user)
+ return head(403) if @runner.is_shared? || @runner.locked?
+
+ runner_project = @runner.assign_to(@project, current_user)
+
+ if runner_project.persisted?
redirect_to admin_runner_path(@runner)
else
redirect_to admin_runner_path(@runner), alert: 'Failed adding runner to project'
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index dd1bc6f5d52..9cc31620d9f 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -36,6 +36,10 @@ class ApplicationController < ActionController::Base
render_404
end
+ rescue_from Gitlab::Access::AccessDeniedError do |exception|
+ render_403
+ end
+
def redirect_back_or_default(default: root_path, options: {})
redirect_to request.referer.present? ? :back : default, options
end
diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb
index a24273fad0b..52dc396af6a 100644
--- a/app/controllers/concerns/membership_actions.rb
+++ b/app/controllers/concerns/membership_actions.rb
@@ -21,29 +21,18 @@ module MembershipActions
def leave
@member = membershipable.members.find_by(user_id: current_user)
- return render_403 unless @member
+ Members::DestroyService.new(@member, current_user).execute
source_type = @member.real_source_type.humanize(capitalize: false)
-
- if can?(current_user, action_member_permission(:destroy, @member), @member)
- notice =
- if @member.request?
- "Your access request to the #{source_type} has been withdrawn."
- else
- "You left the \"#{@member.source.human_name}\" #{source_type}."
- end
- @member.destroy
-
- redirect_to [:dashboard, @member.real_source_type.tableize], notice: notice
- else
- if cannot_leave?
- alert = "You can not leave the \"#{@member.source.human_name}\" #{source_type}."
- alert << " Transfer or delete the #{source_type}."
- redirect_to polymorphic_url(membershipable), alert: alert
+ notice =
+ if @member.request?
+ "Your access request to the #{source_type} has been withdrawn."
else
- render_403
+ "You left the \"#{@member.source.human_name}\" #{source_type}."
end
- end
+ redirect_path = @member.request? ? @member.source : [:dashboard, @member.real_source_type.tableize]
+
+ redirect_to redirect_path, notice: notice
end
protected
@@ -51,8 +40,4 @@ module MembershipActions
def membershipable
raise NotImplementedError
end
-
- def cannot_leave?
- raise NotImplementedError
- end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index d0f2e2949f0..2c49fe3833e 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -36,9 +36,7 @@ class Groups::GroupMembersController < Groups::ApplicationController
def destroy
@group_member = @group.group_members.find(params[:id])
- return render_403 unless can?(current_user, :destroy_group_member, @group_member)
-
- @group_member.destroy
+ Members::DestroyService.new(@group_member, current_user).execute
respond_to do |format|
format.html { redirect_to group_group_members_path(@group), notice: 'User was successfully removed from group.' }
@@ -68,8 +66,4 @@ class Groups::GroupMembersController < Groups::ApplicationController
# MembershipActions concern
alias_method :membershipable, :group
-
- def cannot_leave?
- @group.last_owner?(current_user)
- end
end
diff --git a/app/controllers/groups/notification_settings_controller.rb b/app/controllers/groups/notification_settings_controller.rb
deleted file mode 100644
index de13b16ccf2..00000000000
--- a/app/controllers/groups/notification_settings_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class Groups::NotificationSettingsController < Groups::ApplicationController
- before_action :authenticate_user!
-
- def update
- notification_setting = current_user.notification_settings_for(group)
- saved = notification_setting.update_attributes(notification_setting_params)
-
- render json: { saved: saved }
- end
-
- private
-
- def notification_setting_params
- params.require(:notification_setting).permit(:level)
- end
-end
diff --git a/app/controllers/notification_settings_controller.rb b/app/controllers/notification_settings_controller.rb
new file mode 100644
index 00000000000..eddd03cc229
--- /dev/null
+++ b/app/controllers/notification_settings_controller.rb
@@ -0,0 +1,36 @@
+class NotificationSettingsController < ApplicationController
+ before_action :authenticate_user!
+
+ def create
+ project = Project.find(params[:project][:id])
+
+ return render_404 unless can?(current_user, :read_project, project)
+
+ @notification_setting = current_user.notification_settings_for(project)
+ @saved = @notification_setting.update_attributes(notification_setting_params)
+
+ render_response
+ end
+
+ def update
+ @notification_setting = current_user.notification_settings.find(params[:id])
+ @saved = @notification_setting.update_attributes(notification_setting_params)
+
+ render_response
+ end
+
+ private
+
+ def render_response
+ render json: {
+ html: view_to_html_string("shared/notifications/_button", notification_setting: @notification_setting),
+ saved: @saved
+ }
+ end
+
+ def notification_setting_params
+ allowed_fields = NotificationSetting::EMAIL_EVENTS.dup
+ allowed_fields << :level
+ params.require(:notification_setting).permit(allowed_fields)
+ end
+end
diff --git a/app/controllers/profiles/notifications_controller.rb b/app/controllers/profiles/notifications_controller.rb
index 40d1906a53f..b8b71d295f6 100644
--- a/app/controllers/profiles/notifications_controller.rb
+++ b/app/controllers/profiles/notifications_controller.rb
@@ -1,13 +1,13 @@
class Profiles::NotificationsController < Profiles::ApplicationController
def show
@user = current_user
- @group_notifications = current_user.notification_settings.for_groups
- @project_notifications = current_user.notification_settings.for_projects
+ @group_notifications = current_user.notification_settings.for_groups.order(:id)
+ @project_notifications = current_user.notification_settings.for_projects.order(:id)
@global_notification_setting = current_user.global_notification_setting
end
def update
- if current_user.update_attributes(user_params) && update_notification_settings
+ if current_user.update_attributes(user_params)
flash[:notice] = "Notification settings saved"
else
flash[:alert] = "Failed to save new settings"
@@ -19,16 +19,4 @@ class Profiles::NotificationsController < Profiles::ApplicationController
def user_params
params.require(:user).permit(:notification_email)
end
-
- def global_notification_setting_params
- params.require(:global_notification_setting).permit(:level)
- end
-
- private
-
- def update_notification_settings
- return true unless global_notification_setting_params
-
- current_user.global_notification_setting.update_attributes(global_notification_setting_params)
- end
end
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 776ba92c9ab..996909a28c6 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -74,7 +74,7 @@ class Projects::ApplicationController < ApplicationController
end
def require_branch_head
- unless @repository.branch_names.include?(@ref)
+ unless @repository.branch_exists?(@ref)
redirect_to(
namespace_project_tree_path(@project.namespace, @project, @ref),
notice: "This action is not allowed unless you are on a branch"
diff --git a/app/controllers/projects/notification_settings_controller.rb b/app/controllers/projects/notification_settings_controller.rb
deleted file mode 100644
index 7d81cc03c73..00000000000
--- a/app/controllers/projects/notification_settings_controller.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-class Projects::NotificationSettingsController < Projects::ApplicationController
- before_action :authenticate_user!
-
- def update
- notification_setting = current_user.notification_settings_for(project)
- saved = notification_setting.update_attributes(notification_setting_params)
-
- render json: { saved: saved }
- end
-
- private
-
- def notification_setting_params
- params.require(:notification_setting).permit(:level)
- end
-end
diff --git a/app/controllers/projects/pipelines_controller.rb b/app/controllers/projects/pipelines_controller.rb
index 127bd1a4318..487963fdcd7 100644
--- a/app/controllers/projects/pipelines_controller.rb
+++ b/app/controllers/projects/pipelines_controller.rb
@@ -54,6 +54,6 @@ class Projects::PipelinesController < Projects::ApplicationController
end
def commit
- @commit ||= @pipeline.commit_data
+ @commit ||= @pipeline.commit
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index 35d067cd029..6ba32d33403 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -50,9 +50,7 @@ class Projects::ProjectMembersController < Projects::ApplicationController
def destroy
@project_member = @project.project_members.find(params[:id])
- return render_403 unless can?(current_user, :destroy_project_member, @project_member)
-
- @project_member.destroy
+ Members::DestroyService.new(@project_member, current_user).execute
respond_to do |format|
format.html do
@@ -98,8 +96,4 @@ class Projects::ProjectMembersController < Projects::ApplicationController
# MembershipActions concern
alias_method :membershipable, :project
-
- def cannot_leave?
- current_user == @project.owner
- end
end
diff --git a/app/controllers/projects/runner_projects_controller.rb b/app/controllers/projects/runner_projects_controller.rb
index bedeb4a295c..dc1a18f8d42 100644
--- a/app/controllers/projects/runner_projects_controller.rb
+++ b/app/controllers/projects/runner_projects_controller.rb
@@ -6,11 +6,13 @@ class Projects::RunnerProjectsController < Projects::ApplicationController
def create
@runner = Ci::Runner.find(params[:runner_project][:runner_id])
+ return head(403) if @runner.is_shared? || @runner.locked?
return head(403) unless current_user.ci_authorized_runners.include?(@runner)
path = runners_path(project)
+ runner_project = @runner.assign_to(project, current_user)
- if @runner.assign_to(project, current_user)
+ if runner_project.persisted?
redirect_to path
else
redirect_to path, alert: 'Failed adding runner to project'
diff --git a/app/controllers/projects/runners_controller.rb b/app/controllers/projects/runners_controller.rb
index 0b4fa572501..53c36635efe 100644
--- a/app/controllers/projects/runners_controller.rb
+++ b/app/controllers/projects/runners_controller.rb
@@ -5,10 +5,9 @@ class Projects::RunnersController < Projects::ApplicationController
layout 'project_settings'
def index
- @runners = project.runners.ordered
- @specific_runners = current_user.ci_authorized_runners.
- where.not(id: project.runners).
- ordered.page(params[:page]).per(20)
+ @project_runners = project.runners.ordered
+ @assignable_runners = current_user.ci_authorized_runners.
+ assignable_for(project).ordered.page(params[:page]).per(20)
@shared_runners = Ci::Runner.shared.active
@shared_runners_count = @shared_runners.count(:all)
end
diff --git a/app/controllers/projects/todos_controller.rb b/app/controllers/projects/todos_controller.rb
index 648d42c56c5..23868d986e9 100644
--- a/app/controllers/projects/todos_controller.rb
+++ b/app/controllers/projects/todos_controller.rb
@@ -1,18 +1,12 @@
class Projects::TodosController < Projects::ApplicationController
- def create
- todos = TodoService.new.mark_todo(issuable, current_user)
-
- render json: {
- todo: todos,
- count: current_user.todos_pending_count,
- }
- end
+ before_action :authenticate_user!, only: [:create]
- def update
- current_user.todos.find_by_id(params[:id]).update(state: :done)
+ def create
+ todo = TodoService.new.mark_todo(issuable, current_user)
render json: {
count: current_user.todos_pending_count,
+ delete_path: dashboard_todo_path(todo)
}
end
@@ -22,7 +16,13 @@ class Projects::TodosController < Projects::ApplicationController
@issuable ||= begin
case params[:issuable_type]
when "issue"
- @project.issues.find(params[:issuable_id])
+ issue = @project.issues.find(params[:issuable_id])
+
+ if can?(current_user, :read_issue, issue)
+ issue
+ else
+ render_404
+ end
when "merge_request"
@project.merge_requests.find(params[:issuable_id])
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 8044c637825..2b1f50fd01e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -1,7 +1,7 @@
class ProjectsController < Projects::ApplicationController
include ExtractsPath
- before_action :authenticate_user!, except: [:show, :activity]
+ before_action :authenticate_user!, except: [:show, :activity, :refs]
before_action :project, except: [:new, :create]
before_action :repository, except: [:new, :create]
before_action :assign_ref_vars, :tree, only: [:show], if: :repo_exists?
@@ -251,6 +251,24 @@ class ProjectsController < Projects::ApplicationController
}
end
+ def refs
+ options = {
+ 'Branches' => @repository.branch_names,
+ }
+
+ unless @repository.tag_count.zero?
+ options['Tags'] = VersionSorter.rsort(@repository.tag_names)
+ end
+
+ # If reference is commit id - we should add it to branch/tag selectbox
+ ref = Addressable::URI.unescape(params[:ref])
+ if ref && options.flatten(2).exclude?(ref) && ref =~ /\A[0-9a-zA-Z]{6,52}\z/
+ options['Commits'] = [ref]
+ end
+
+ render json: options.to_json
+ end
+
private
def determine_layout
@@ -285,8 +303,14 @@ class ProjectsController < Projects::ApplicationController
project.repository_exists? && !project.empty_repo?
end
- # Override get_id from ExtractsPath, which returns the branch and file path
+ # Override extract_ref from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
+ # This way we avoid to access the repository.ref_names.
+ def extract_ref(_id)
+ [get_id, '']
+ end
+
+ # Override get_id from ExtractsPath in this case is just the root of the default branch.
def get_id
project.repository.root_ref
end
diff --git a/app/helpers/application_helper.rb b/app/helpers/application_helper.rb
index 439b015b3b8..41859841834 100644
--- a/app/helpers/application_helper.rb
+++ b/app/helpers/application_helper.rb
@@ -101,22 +101,6 @@ module ApplicationHelper
'Never'
end
- def grouped_options_refs
- repository = @project.repository
-
- options = [
- ['Branches', repository.branch_names],
- ['Tags', VersionSorter.rsort(repository.tag_names)]
- ]
-
- # If reference is commit id - we should add it to branch/tag selectbox
- if @ref && !options.flatten.include?(@ref) && @ref =~ /\A[0-9a-zA-Z]{6,52}\z/
- options << ['Commit', [@ref]]
- end
-
- grouped_options_for_select(options, @ref || @project.default_branch)
- end
-
# Define whenever show last push event
# with suggestion to create MR
def show_last_push_widget?(event)
@@ -132,7 +116,7 @@ module ApplicationHelper
return false if project.merge_requests.where(source_branch: event.branch_name).opened.any?
# Skip if user removed branch right after that
- return false unless project.repository.branch_names.include?(event.branch_name)
+ return false unless project.repository.branch_exists?(event.branch_name)
true
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 5b54b34070c..4b4bc3d4276 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -29,7 +29,7 @@ module BlobHelper
if !on_top_of_branch?(project, ref)
button_tag "Edit", class: "btn disabled has-tooltip btn-file-option", title: "You can only edit files when you are on a branch", data: { container: 'body' }
elsif can_edit_blob?(blob, project, ref)
- link_to "Edit", edit_path, class: 'btn btn-file-option'
+ link_to "Edit", edit_path, class: 'btn btn-sm'
elsif can?(current_user, :fork_project, project)
continue_params = {
to: edit_path,
@@ -186,12 +186,16 @@ module BlobHelper
end
def gitignore_names
- return @gitignore_names if defined?(@gitignore_names)
+ @gitignore_names ||=
+ Gitlab::Template::Gitignore.categories.keys.map do |k|
+ [k, Gitlab::Template::Gitignore.by_category(k).map { |t| { name: t.name } }]
+ end.to_h
+ end
- @gitignore_names = {
- Global: Gitlab::Gitignore.global.map { |gitignore| { name: gitignore.name } },
- # Note that the key here doesn't cover it really
- Languages: Gitlab::Gitignore.languages_frameworks.map{ |gitignore| { name: gitignore.name } }
- }
+ def gitlab_ci_ymls
+ @gitlab_ci_ymls ||=
+ Gitlab::Template::GitlabCiYml.categories.keys.map do |k|
+ [k, Gitlab::Template::GitlabCiYml.by_category(k).map { |t| { name: t.name } }]
+ end.to_h
end
end
diff --git a/app/helpers/branches_helper.rb b/app/helpers/branches_helper.rb
index 3ee3fc74f0c..c533659b600 100644
--- a/app/helpers/branches_helper.rb
+++ b/app/helpers/branches_helper.rb
@@ -10,7 +10,7 @@ module BranchesHelper
end
def can_push_branch?(project, branch_name)
- return false unless project.repository.branch_names.include?(branch_name)
+ return false unless project.repository.branch_exists?(branch_name)
::Gitlab::GitAccess.new(current_user, project).can_push_to_branch?(branch_name)
end
diff --git a/app/helpers/gitlab_markdown_helper.rb b/app/helpers/gitlab_markdown_helper.rb
index 067a00660aa..1a259656f31 100644
--- a/app/helpers/gitlab_markdown_helper.rb
+++ b/app/helpers/gitlab_markdown_helper.rb
@@ -50,8 +50,6 @@ module GitlabMarkdownHelper
context[:project] ||= @project
- text = Banzai.pre_process(text, context)
-
html = Banzai.render(text, context)
context.merge!(
@@ -185,4 +183,17 @@ module GitlabMarkdownHelper
''
end
end
+
+ def markdown_toolbar_button(options = {})
+ data = options[:data].merge({ container: "body" })
+ content_tag :button,
+ type: "button",
+ class: "toolbar-btn js-md has-tooltip hidden-xs",
+ tabindex: -1,
+ data: data,
+ title: options[:title],
+ aria: { label: options[:title] } do
+ icon(options[:icon])
+ end
+ end
end
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index 8dbc51a689f..8231ce49fac 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -67,9 +67,9 @@ module IssuablesHelper
end
end
- def has_todo(issuable)
- unless current_user.nil?
- current_user.todos.find_by(target_id: issuable.id, state: :pending)
+ def issuable_todo(issuable)
+ if current_user
+ current_user.todos.find_by(target: issuable, state: :pending)
end
end
diff --git a/app/helpers/members_helper.rb b/app/helpers/members_helper.rb
index 877c77050be..ec106418f2d 100644
--- a/app/helpers/members_helper.rb
+++ b/app/helpers/members_helper.rb
@@ -6,6 +6,12 @@ module MembersHelper
"#{action}_#{member.type.underscore}".to_sym
end
+ def default_show_roles(member)
+ can?(current_user, action_member_permission(:update, member), member) ||
+ can?(current_user, action_member_permission(:destroy, member), member) ||
+ can?(current_user, action_member_permission(:admin, member), member.source)
+ end
+
def remove_member_message(member, user: nil)
user = current_user if defined?(current_user)
diff --git a/app/helpers/notifications_helper.rb b/app/helpers/notifications_helper.rb
index 50c21fc0d49..77783cd7640 100644
--- a/app/helpers/notifications_helper.rb
+++ b/app/helpers/notifications_helper.rb
@@ -34,7 +34,7 @@ module NotificationsHelper
def notification_description(level)
case level.to_sym
when :participating
- 'You will only receive notifications from related resources'
+ 'You will only receive notifications for threads you have participated in'
when :mention
'You will receive notifications only for comments in which you were @mentioned'
when :watch
@@ -43,6 +43,8 @@ module NotificationsHelper
'You will not get any notifications via email'
when :global
'Use your global notification setting'
+ when :custom
+ 'You will only receive notifications for the events you choose'
end
end
@@ -62,22 +64,14 @@ module NotificationsHelper
end
end
- def notification_level_radio_buttons
- html = ""
-
- NotificationSetting.levels.each_key do |level|
- level = level.to_sym
- next if level == :global
-
- html << content_tag(:div, class: "radio") do
- content_tag(:label, { value: level }) do
- radio_button_tag(:"global_notification_setting[level]", level, @global_notification_setting.level.to_sym == level) +
- content_tag(:div, level.to_s.capitalize, class: "level-title") +
- content_tag(:p, notification_description(level))
- end
- end
- end
+ # Identifier to trigger individually dropdowns and custom settings modals in the same view
+ def notifications_menu_identifier(type, notification_setting)
+ "#{type}-#{notification_setting.user_id}-#{notification_setting.source_id}-#{notification_setting.source_type}"
+ end
- html.html_safe
+ # Create hidden field to send notification setting source to controller
+ def hidden_setting_source_input(notification_setting)
+ return unless notification_setting.source_type
+ hidden_field_tag "#{notification_setting.source_type.downcase}[id]", notification_setting.source_id
end
end
diff --git a/app/helpers/todos_helper.rb b/app/helpers/todos_helper.rb
index e1d8517f712..a832a6c8df7 100644
--- a/app/helpers/todos_helper.rb
+++ b/app/helpers/todos_helper.rb
@@ -12,7 +12,7 @@ module TodosHelper
when Todo::ASSIGNED then 'assigned you'
when Todo::MENTIONED then 'mentioned you on'
when Todo::BUILD_FAILED then 'The build failed for your'
- when Todo::MARKED then 'marked this as a Todo for'
+ when Todo::MARKED then 'added a todo for'
end
end
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 6dde2e9847d..45311690293 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -12,6 +12,11 @@ module Emails
@member_id = member_id
admins = member_source.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ # A project in a group can have no explicit owners/masters, in that case
+ # we fallbacks to the group's owners/masters.
+ if admins.empty? && member_source.respond_to?(:group) && member_source.group
+ admins = member_source.group.members.owners_and_masters.includes(:user).pluck(:notification_email)
+ end
mail(to: admins,
subject: subject("Request to join the #{member_source.human_name} #{member_source.model_name.singular}"))
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index d618c84e983..2b0bec33131 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -300,18 +300,12 @@ module Ci
project.valid_runners_token? token
end
- def can_be_served?(runner)
- return false unless has_tags? || runner.run_untagged?
-
- (tag_list - runner.tag_list).empty?
- end
-
def has_tags?
tag_list.any?
end
def any_runners_online?
- project.any_runners? { |runner| runner.active? && runner.online? && can_be_served?(runner) }
+ project.any_runners? { |runner| runner.active? && runner.online? && runner.can_pick?(self) }
end
def stuck?
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 5b264ecffc5..ca5a685dd11 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -37,22 +37,22 @@ module Ci
end
def git_author_name
- commit_data.author_name if commit_data
+ commit.try(:author_name)
end
def git_author_email
- commit_data.author_email if commit_data
+ commit.try(:author_email)
end
def git_commit_message
- commit_data.message if commit_data
+ commit.try(:message)
end
def short_sha
Ci::Pipeline.truncate_sha(sha)
end
- def commit_data
+ def commit
@commit ||= project.commit(sha)
rescue
nil
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index adb65292208..b64ec79ec2b 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -4,7 +4,7 @@ module Ci
LAST_CONTACT_TIME = 5.minutes.ago
AVAILABLE_SCOPES = %w[specific shared active paused online]
- FORM_EDITABLE = %i[description tag_list active run_untagged]
+ FORM_EDITABLE = %i[description tag_list active run_untagged locked]
has_many :builds, class_name: 'Ci::Build'
has_many :runner_projects, dependent: :destroy, class_name: 'Ci::RunnerProject'
@@ -26,6 +26,13 @@ module Ci
.where("ci_runner_projects.gl_project_id = :project_id OR ci_runners.is_shared = true", project_id: project_id)
end
+ scope :assignable_for, ->(project) do
+ # FIXME: That `to_sql` is needed to workaround a weird Rails bug.
+ # Without that, placeholders would miss one and couldn't match.
+ where(locked: false).
+ where.not("id IN (#{project.runners.select(:id).to_sql})").specific
+ end
+
validate :tag_constraints
acts_as_taggable
@@ -56,7 +63,7 @@ module Ci
def assign_to(project, current_user = nil)
self.is_shared = false if shared?
self.save
- project.runner_projects.create!(runner_id: self.id)
+ project.runner_projects.create(runner_id: self.id)
end
def display_name
@@ -91,6 +98,10 @@ module Ci
!shared?
end
+ def can_pick?(build)
+ assignable_for?(build.project) && accepting_tags?(build)
+ end
+
def only_for?(project)
projects == [project]
end
@@ -111,5 +122,13 @@ module Ci
'can not be empty when runner is not allowed to pick untagged jobs')
end
end
+
+ def assignable_for?(project)
+ !locked? || projects.exists?(id: project.id)
+ end
+
+ def accepting_tags?(build)
+ (run_untagged? || build.has_tags?) && (build.tag_list - tag_list).empty?
+ end
end
end
diff --git a/app/models/commit.rb b/app/models/commit.rb
index d69d518fadd..174ccbaea6c 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -271,6 +271,32 @@ class Commit
merged_merge_request ? 'merge request' : 'commit'
end
+ # Get the URI type of the given path
+ #
+ # Used to build URLs to files in the repository in GFM.
+ #
+ # path - String path to check
+ #
+ # Examples:
+ #
+ # uri_type('doc/README.md') # => :blob
+ # uri_type('doc/logo.png') # => :raw
+ # uri_type('doc/api') # => :tree
+ # uri_type('not/found') # => :nil
+ #
+ # Returns a symbol
+ def uri_type(path)
+ entry = @raw.tree.path(path)
+ if entry[:type] == :blob
+ blob = Gitlab::Git::Blob.new(name: entry[:name])
+ blob.image? ? :raw : :blob
+ else
+ entry[:type]
+ end
+ rescue Rugged::TreeError
+ nil
+ end
+
private
def repo_changes
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index ab13db4b297..e437e3417a8 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -8,6 +8,8 @@ class CommitStatus < ActiveRecord::Base
belongs_to :pipeline, class_name: 'Ci::Pipeline', foreign_key: :commit_id, touch: true
belongs_to :user
+ delegate :commit, to: :pipeline
+
validates :pipeline, presence: true, unless: :importing?
validates_presence_of :name
diff --git a/app/models/concerns/participable.rb b/app/models/concerns/participable.rb
index 9056722f45e..9822844357d 100644
--- a/app/models/concerns/participable.rb
+++ b/app/models/concerns/participable.rb
@@ -53,6 +53,16 @@ module Participable
#
# Returns an Array of User instances.
def participants(current_user = nil)
+ @participants ||= Hash.new do |hash, user|
+ hash[user] = raw_participants(user)
+ end
+
+ @participants[current_user]
+ end
+
+ private
+
+ def raw_participants(current_user = nil)
current_user ||= author
ext = Gitlab::ReferenceExtractor.new(project, current_user)
participants = Set.new
diff --git a/app/models/concerns/referable.rb b/app/models/concerns/referable.rb
index ce064f675ae..dee940a3f88 100644
--- a/app/models/concerns/referable.rb
+++ b/app/models/concerns/referable.rb
@@ -49,6 +49,10 @@ module Referable
raise NotImplementedError, "#{self} does not implement #{__method__}"
end
+ def reference_valid?(reference)
+ true
+ end
+
def link_reference_pattern(route, pattern)
%r{
(?<url>
diff --git a/app/models/issue.rb b/app/models/issue.rb
index 1bdf9c011b2..3c5859194b4 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -83,6 +83,10 @@ class Issue < ActiveRecord::Base
@link_reference_pattern ||= super("issues", /(?<issue>\d+)/)
end
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
+ end
+
def self.sort(method, excluded_labels: [])
case method.to_s
when 'due_date_asc' then order_due_date_asc
diff --git a/app/models/key.rb b/app/models/key.rb
index 0532e84f47d..b9bc38a0436 100644
--- a/app/models/key.rb
+++ b/app/models/key.rb
@@ -9,7 +9,7 @@ class Key < ActiveRecord::Base
before_validation :strip_white_space, :generate_fingerprint
validates :title, presence: true, length: { within: 0..255 }
- validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }, uniqueness: true
+ validates :key, presence: true, length: { within: 0..5000 }, format: { with: /\A(ssh|ecdsa)-.*\Z/ }
validates :key, format: { without: /\n|\r/, message: 'should be a single line' }
validates :fingerprint, uniqueness: true, presence: { message: 'cannot be generated' }
diff --git a/app/models/member.rb b/app/models/member.rb
index 4ee3f1bb5c2..c74a16367db 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -48,7 +48,6 @@ class Member < ActiveRecord::Base
after_create :post_create_hook, unless: [:pending?, :importing?]
after_update :post_update_hook, unless: [:pending?, :importing?]
after_destroy :post_destroy_hook, unless: :pending?
- after_destroy :post_decline_request, if: :request?
delegate :name, :username, :email, to: :user, prefix: true
@@ -188,7 +187,7 @@ class Member < ActiveRecord::Base
end
def send_request
- # override in subclass
+ notification_service.new_access_request(self)
end
def post_create_hook
@@ -215,10 +214,6 @@ class Member < ActiveRecord::Base
post_create_hook
end
- def post_decline_request
- # override in subclass
- end
-
def system_hook_service
SystemHooksService.new
end
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index 363db877968..2f13d339c89 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -33,12 +33,6 @@ class GroupMember < Member
super
end
- def send_request
- notification_service.new_group_access_request(self)
-
- super
- end
-
def post_create_hook
notification_service.new_group_member(self)
@@ -64,10 +58,4 @@ class GroupMember < Member
super
end
-
- def post_decline_request
- notification_service.decline_group_access_request(self)
-
- super
- end
end
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 250ee04fd1d..e9d3a82ba15 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -111,12 +111,6 @@ class ProjectMember < Member
super
end
- def send_request
- notification_service.new_project_access_request(self)
-
- super
- end
-
def post_create_hook
unless owner?
event_service.join_project(self.project, self.user)
@@ -152,12 +146,6 @@ class ProjectMember < Member
super
end
- def post_decline_request
- notification_service.decline_project_access_request(self)
-
- super
- end
-
def event_service
EventCreateService.new
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 73bf182ec9f..36bc98bdb1e 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -133,6 +133,10 @@ class MergeRequest < ActiveRecord::Base
@link_reference_pattern ||= super("merge_requests", /(?<merge_request>\d+)/)
end
+ def self.reference_valid?(reference)
+ reference.to_i > 0 && reference.to_i <= Gitlab::Database::MAX_INT_VALUE
+ end
+
# Returns all the merge requests from an ActiveRecord:Relation.
#
# This method uses a UNION as it usually operates on the result of
diff --git a/app/models/notification_setting.rb b/app/models/notification_setting.rb
index 0ce87968e46..d41fc7073c6 100644
--- a/app/models/notification_setting.rb
+++ b/app/models/notification_setting.rb
@@ -1,5 +1,5 @@
class NotificationSetting < ActiveRecord::Base
- enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0 }
+ enum level: { global: 3, watch: 2, mention: 4, participating: 1, disabled: 0, custom: 5 }
default_value_for :level, NotificationSetting.levels[:global]
@@ -15,6 +15,24 @@ class NotificationSetting < ActiveRecord::Base
scope :for_groups, -> { where(source_type: 'Namespace') }
scope :for_projects, -> { where(source_type: 'Project') }
+ EMAIL_EVENTS = [
+ :new_note,
+ :new_issue,
+ :reopen_issue,
+ :close_issue,
+ :reassign_issue,
+ :new_merge_request,
+ :reopen_merge_request,
+ :close_merge_request,
+ :reassign_merge_request,
+ :merge_merge_request
+ ]
+
+ store :events, accessors: EMAIL_EVENTS, coder: JSON
+
+ before_create :set_events
+ before_save :events_to_boolean
+
def self.find_or_create_for(source)
setting = find_or_initialize_by(source: source)
@@ -24,4 +42,21 @@ class NotificationSetting < ActiveRecord::Base
setting
end
+
+ # Set all event attributes to false when level is not custom or being initialized for UX reasons
+ def set_events
+ return if custom?
+
+ EMAIL_EVENTS.each do |event|
+ events[event] = false
+ end
+ end
+
+ # Validates store accessors values as boolean
+ # It is a text field so it does not cast correct boolean values in JSON
+ def events_to_boolean
+ EMAIL_EVENTS.each do |event|
+ events[event] = ActiveRecord::ConnectionAdapters::Column::TRUE_VALUES.include?(events[event])
+ end
+ end
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index bbd7682d8e7..221c87164ca 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -191,8 +191,12 @@ class Repository
end
end
+ def ref_names
+ branch_names + tag_names
+ end
+
def branch_names
- cache.fetch(:branch_names) { branches.map(&:name) }
+ @branch_names ||= cache.fetch(:branch_names) { branches.map(&:name) }
end
def branch_exists?(branch_name)
@@ -267,6 +271,7 @@ class Repository
def expire_branches_cache
cache.expire(:branch_names)
+ @branch_names = nil
@local_branches = nil
end
@@ -332,10 +337,6 @@ class Repository
@lookup_cache ||= {}
end
- def expire_branch_names
- cache.expire(:branch_names)
- end
-
def expire_avatar_cache(branch_name = nil, revision = nil)
# Avatars are pulled from the default branch, thus if somebody pushes to a
# different branch there's no need to expire anything.
diff --git a/app/models/user.rb b/app/models/user.rb
index 2e458329cb9..876ccc69d8d 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -487,9 +487,8 @@ class User < ActiveRecord::Base
events.recent.find do |event|
project = Project.find_by_id(event.project_id)
next unless project
- repo = project.repository
- if repo.branch_names.include?(event.branch_name)
+ if project.repository.branch_exists?(event.branch_name)
merge_requests = MergeRequest.where("created_at >= ?", event.created_at).
where(source_project_id: project.id,
source_branch: event.branch_name)
diff --git a/app/services/ci/register_build_service.rb b/app/services/ci/register_build_service.rb
index f0ed09a629a..9a187f5d694 100644
--- a/app/services/ci/register_build_service.rb
+++ b/app/services/ci/register_build_service.rb
@@ -21,7 +21,7 @@ module Ci
end
build = builds.find do |build|
- build.can_be_served?(current_runner)
+ current_runner.can_pick?(build)
end
if build
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
new file mode 100644
index 00000000000..15358f80208
--- /dev/null
+++ b/app/services/members/destroy_service.rb
@@ -0,0 +1,21 @@
+module Members
+ class DestroyService < BaseService
+ attr_accessor :member, :current_user
+
+ def initialize(member, user)
+ @member, @current_user = member, user
+ end
+
+ def execute
+ unless member && can?(current_user, "destroy_#{member.type.underscore}".to_sym, member)
+ raise Gitlab::Access::AccessDeniedError
+ end
+
+ member.destroy
+
+ if member.request? && member.user != current_user
+ notification_service.decline_access_request(member)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index 1b48899bb0a..7fe57747265 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -83,7 +83,7 @@ module MergeRequests
closes_issue = "Closes ##{iid}"
if merge_request.description.present?
- merge_request.description << closes_issue.prepend("\n")
+ merge_request.description += closes_issue.prepend("\n")
else
merge_request.description = closes_issue
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index c125b8aff29..590350a11e5 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -29,9 +29,10 @@ class NotificationService
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the issue's labels
+ # * users with custom level checked with "new issue"
#
def new_issue(issue, current_user)
- new_resource_email(issue, issue.project, 'new_issue_email')
+ new_resource_email(issue, issue.project, :new_issue_email)
end
# When we close an issue we should send an email to:
@@ -39,18 +40,20 @@ class NotificationService
# * issue author if their notification level is not Disabled
# * issue assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
+ # * users with custom level checked with "close issue"
#
def close_issue(issue, current_user)
- close_resource_email(issue, issue.project, current_user, 'closed_issue_email')
+ close_resource_email(issue, issue.project, current_user, :closed_issue_email)
end
# When we reassign an issue we should send an email to:
#
# * issue old assignee if their notification level is not Disabled
# * issue new assignee if their notification level is not Disabled
+ # * users with custom level checked with "reassign issue"
#
def reassigned_issue(issue, current_user)
- reassign_resource_email(issue, issue.project, current_user, 'reassigned_issue_email')
+ reassign_resource_email(issue, issue.project, current_user, :reassigned_issue_email)
end
# When we add labels to an issue we should send an email to:
@@ -58,7 +61,7 @@ class NotificationService
# * watchers of the issue's labels
#
def relabeled_issue(issue, added_labels, current_user)
- relabeled_resource_email(issue, added_labels, current_user, 'relabeled_issue_email')
+ relabeled_resource_email(issue, added_labels, current_user, :relabeled_issue_email)
end
# When create a merge request we should send an email to:
@@ -66,18 +69,20 @@ class NotificationService
# * mr assignee if their notification level is not Disabled
# * project team members with notification level higher then Participating
# * watchers of the mr's labels
+ # * users with custom level checked with "new merge request"
#
def new_merge_request(merge_request, current_user)
- new_resource_email(merge_request, merge_request.target_project, 'new_merge_request_email')
+ new_resource_email(merge_request, merge_request.target_project, :new_merge_request_email)
end
# When we reassign a merge_request we should send an email to:
#
# * merge_request old assignee if their notification level is not Disabled
# * merge_request assignee if their notification level is not Disabled
+ # * users with custom level checked with "reassign merge request"
#
def reassigned_merge_request(merge_request, current_user)
- reassign_resource_email(merge_request, merge_request.target_project, current_user, 'reassigned_merge_request_email')
+ reassign_resource_email(merge_request, merge_request.target_project, current_user, :reassigned_merge_request_email)
end
# When we add labels to a merge request we should send an email to:
@@ -85,15 +90,15 @@ class NotificationService
# * watchers of the mr's labels
#
def relabeled_merge_request(merge_request, added_labels, current_user)
- relabeled_resource_email(merge_request, added_labels, current_user, 'relabeled_merge_request_email')
+ relabeled_resource_email(merge_request, added_labels, current_user, :relabeled_merge_request_email)
end
def close_mr(merge_request, current_user)
- close_resource_email(merge_request, merge_request.target_project, current_user, 'closed_merge_request_email')
+ close_resource_email(merge_request, merge_request.target_project, current_user, :closed_merge_request_email)
end
def reopen_issue(issue, current_user)
- reopen_resource_email(issue, issue.project, current_user, 'issue_status_changed_email', 'reopened')
+ reopen_resource_email(issue, issue.project, current_user, :issue_status_changed_email, 'reopened')
end
def merge_mr(merge_request, current_user)
@@ -101,7 +106,7 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- 'merged_merge_request_email'
+ :merged_merge_request_email
)
end
@@ -110,7 +115,7 @@ class NotificationService
merge_request,
merge_request.target_project,
current_user,
- 'merge_request_status_email',
+ :merge_request_status_email,
'reopened'
)
end
@@ -153,6 +158,9 @@ class NotificationService
# Merge project watchers
recipients = add_project_watchers(recipients, note.project)
+ # Merge project with custom notification
+ recipients = add_custom_notifications(recipients, note.project, :new_note)
+
# Reject users with Mention notification level, except those mentioned in _this_ note.
recipients = reject_mention_users(recipients - mentioned_users, note.project)
recipients = recipients + mentioned_users
@@ -173,15 +181,16 @@ class NotificationService
end
end
- # Project access request
- def new_project_access_request(project_member)
- mailer.member_access_requested_email(project_member.real_source_type, project_member.id).deliver_later
+ # Members
+ def new_access_request(member)
+ mailer.member_access_requested_email(member.real_source_type, member.id).deliver_later
end
- def decline_project_access_request(project_member)
- mailer.member_access_denied_email(project_member.real_source_type, project_member.project.id, project_member.user.id).deliver_later
+ def decline_access_request(member)
+ mailer.member_access_denied_email(member.real_source_type, member.source_id, member.user_id).deliver_later
end
+ # Project invite
def invite_project_member(project_member, token)
mailer.member_invited_email(project_member.real_source_type, project_member.id, token).deliver_later
end
@@ -208,15 +217,7 @@ class NotificationService
mailer.member_access_granted_email(project_member.real_source_type, project_member.id).deliver_later
end
- # Group access request
- def new_group_access_request(group_member)
- mailer.member_access_requested_email(group_member.real_source_type, group_member.id).deliver_later
- end
-
- def decline_group_access_request(group_member)
- mailer.member_access_denied_email(group_member.real_source_type, group_member.group.id, group_member.user.id).deliver_later
- end
-
+ # Group invite
def invite_group_member(group_member, token)
mailer.member_invited_email(group_member.real_source_type, group_member.id, token).deliver_later
end
@@ -276,12 +277,31 @@ class NotificationService
protected
+ # Get project/group users with CUSTOM notification level
+ def add_custom_notifications(recipients, project, action)
+ user_ids = []
+
+ # Users with a notification setting on group or project
+ user_ids += notification_settings_for(project, :custom, action)
+ user_ids += notification_settings_for(project.group, :custom, action)
+
+ # Users with global level custom
+ users_with_project_level_global = notification_settings_for(project, :global)
+ users_with_group_level_global = notification_settings_for(project.group, :global)
+
+ global_users_ids = users_with_project_level_global.concat(users_with_group_level_global)
+ user_ids += users_with_global_level_custom(global_users_ids, action)
+
+ recipients.concat(User.find(user_ids))
+ end
+
# Get project users with WATCH notification level
def project_watchers(project)
- project_members = project_member_notification(project)
+ project_members = notification_settings_for(project)
+
+ users_with_project_level_global = notification_settings_for(project, :global)
+ users_with_group_level_global = notification_settings_for(project.group, :global)
- users_with_project_level_global = project_member_notification(project, :global)
- users_with_group_level_global = group_member_notification(project, :global)
users = users_with_global_level_watch([users_with_project_level_global, users_with_group_level_global].flatten.uniq)
users_with_project_setting = select_project_member_setting(project, users_with_project_level_global, users)
@@ -290,33 +310,39 @@ class NotificationService
User.where(id: users_with_project_setting.concat(users_with_group_setting).uniq).to_a
end
- def project_member_notification(project, notification_level=nil)
+ def notification_settings_for(resource, notification_level = nil, action = nil)
+ return [] unless resource
+
if notification_level
- project.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
+ settings = resource.notification_settings.where(level: NotificationSetting.levels[notification_level])
+ settings = settings.select { |setting| setting.events[action] } if action.present?
+ settings.map(&:user_id)
else
- project.notification_settings.pluck(:user_id)
+ resource.notification_settings.pluck(:user_id)
end
end
- def group_member_notification(project, notification_level)
- if project.group
- project.group.notification_settings.where(level: NotificationSetting.levels[notification_level]).pluck(:user_id)
- else
- []
- end
+ def users_with_global_level_watch(ids)
+ settings_with_global_level_of(:watch, ids).pluck(:user_id)
end
- def users_with_global_level_watch(ids)
+ def users_with_global_level_custom(ids, action)
+ settings = settings_with_global_level_of(:custom, ids)
+ settings = settings.select { |setting| setting.events[action] }
+ settings.map(&:user_id)
+ end
+
+ def settings_with_global_level_of(level, ids)
NotificationSetting.where(
user_id: ids,
source_type: nil,
- level: NotificationSetting.levels[:watch]
- ).pluck(:user_id)
+ level: NotificationSetting.levels[level]
+ )
end
# Build a list of users based on project notifcation settings
def select_project_member_setting(project, global_setting, users_global_level_watch)
- users = project_member_notification(project, :watch)
+ users = notification_settings_for(project, :watch)
# If project setting is global, add to watch list if global setting is watch
global_setting.each do |user_id|
@@ -330,7 +356,7 @@ class NotificationService
# Build a list of users based on group notification settings
def select_group_member_setting(project, project_members, global_setting, users_global_level_watch)
- uids = group_member_notification(project, :watch)
+ uids = notification_settings_for(project, :watch)
# Group setting is watch, add to users list if user is not project member
users = []
@@ -351,7 +377,7 @@ class NotificationService
end
def add_project_watchers(recipients, project)
- recipients.concat(project_watchers(project)).compact.uniq
+ recipients.concat(project_watchers(project)).compact
end
# Remove users with disabled notifications from array
@@ -436,7 +462,7 @@ class NotificationService
end
def new_resource_email(target, project, method)
- recipients = build_recipients(target, project, target.author, action: :new)
+ recipients = build_recipients(target, project, target.author, action: "new")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id).deliver_later
@@ -444,7 +470,8 @@ class NotificationService
end
def close_resource_email(target, project, current_user, method)
- recipients = build_recipients(target, project, current_user)
+ action = method == :merged_merge_request_email ? "merge" : "close"
+ recipients = build_recipients(target, project, current_user, action: action)
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, current_user.id).deliver_later
@@ -455,7 +482,7 @@ class NotificationService
previous_assignee_id = previous_record(target, 'assignee_id')
previous_assignee = User.find_by(id: previous_assignee_id) if previous_assignee_id
- recipients = build_recipients(target, project, current_user, action: :reassign, previous_assignee: previous_assignee)
+ recipients = build_recipients(target, project, current_user, action: "reassign", previous_assignee: previous_assignee)
recipients.each do |recipient|
mailer.send(
@@ -478,7 +505,7 @@ class NotificationService
end
def reopen_resource_email(target, project, current_user, method, status)
- recipients = build_recipients(target, project, current_user)
+ recipients = build_recipients(target, project, current_user, action: "reopen")
recipients.each do |recipient|
mailer.send(method, recipient.id, target.id, status, current_user.id).deliver_later
@@ -486,14 +513,20 @@ class NotificationService
end
def build_recipients(target, project, current_user, action: nil, previous_assignee: nil)
+ custom_action = build_custom_key(action, target)
+
recipients = target.participants(current_user)
recipients = add_project_watchers(recipients, project)
+
+ recipients = add_custom_notifications(recipients, project, custom_action)
recipients = reject_mention_users(recipients, project)
+ recipients = recipients.uniq
+
# Re-assign is considered as a mention of the new assignee so we add the
# new assignee to the list of recipients after we rejected users with
# the "on mention" notification level
- if action == :reassign
+ if [:reassign_merge_request, :reassign_issue].include?(custom_action)
recipients << previous_assignee if previous_assignee
recipients << target.assignee
end
@@ -501,7 +534,7 @@ class NotificationService
recipients = reject_muted_users(recipients, project)
recipients = add_subscribed_users(recipients, target)
- if action == :new
+ if [:new_issue, :new_merge_request].include?(custom_action)
recipients = add_labels_subscribers(recipients, target)
end
@@ -531,4 +564,10 @@ class NotificationService
end
end
end
+
+ # Build event key to search on custom notification level
+ # Check NotificationSetting::EMAIL_EVENTS
+ def build_custom_key(action, object)
+ "#{action}_#{object.class.name.underscore}".to_sym
+ end
end
diff --git a/app/services/projects/import_export/export_service.rb b/app/services/projects/import_export/export_service.rb
index d6752377ce5..80c7193efcb 100644
--- a/app/services/projects/import_export/export_service.rb
+++ b/app/services/projects/import_export/export_service.rb
@@ -50,7 +50,7 @@ module Projects
end
def notify_error
- notification_service.project_not_exported(@project, @current_user, @shared.errors.join(', '))
+ notification_service.project_not_exported(@project, @current_user, @shared.errors)
end
end
end
diff --git a/app/views/admin/appearances/_form.html.haml b/app/views/admin/appearances/_form.html.haml
index d88f3ad314d..dc083e50178 100644
--- a/app/views/admin/appearances/_form.html.haml
+++ b/app/views/admin/appearances/_form.html.haml
@@ -46,7 +46,7 @@
Maximum file size is 1MB. Pages are optimized for a 72x72 px header logo
.form-actions
- = f.submit 'Save', class: 'btn btn-save'
+ = f.submit 'Save', class: 'btn btn-save append-right-10'
- if @appearance.persisted?
= link_to 'Preview last save', preview_admin_appearances_path, class: 'btn', target: '_blank'
diff --git a/app/views/admin/appearances/preview.html.haml b/app/views/admin/appearances/preview.html.haml
index dd4a64e80bc..6c51639b840 100644
--- a/app/views/admin/appearances/preview.html.haml
+++ b/app/views/admin/appearances/preview.html.haml
@@ -1,29 +1,9 @@
- page_title "Preview | Appearance"
-%h3.page-title
- Appearance settings - Preview
-%hr
+.login-box
+ .login-heading
+ %h3 Existing user? Sign in
+ %form
+ = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
+ = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
+ = button_tag "Sign in", class: "btn-create btn"
-.ui-box
- .title
- Sign-in page
- %div
- .login-page
- .container
- .content
- .login-title
- %h1= brand_title
- %hr
- .container
- .content
- .row
- .col-sm-7
- .brand-image
- = brand_image
- .brand_text
- = brand_text
- .col-sm-4
- .login-box
- %h3.page-title Sign in
- = text_field_tag :login, nil, class: "form-control top", placeholder: "Username or Email"
- = password_field_tag :password, nil, class: "form-control bottom", placeholder: "Password"
- = button_tag "Sign in", class: "btn-create btn"
diff --git a/app/views/admin/appearances/show.html.haml b/app/views/admin/appearances/show.html.haml
index 089e8e4cb7a..454b779842c 100644
--- a/app/views/admin/appearances/show.html.haml
+++ b/app/views/admin/appearances/show.html.haml
@@ -1,7 +1,9 @@
- page_title "Appearance"
+
%h3.page-title
Appearance settings
%p.light
You can modify the look and feel of GitLab here
+%hr
= render 'form'
diff --git a/app/views/admin/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index e9c7ca9d5aa..ecc46d86afe 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -1,4 +1,5 @@
- page_title "Settings"
+
%h3.page-title Settings
%hr
= render 'form'
diff --git a/app/views/admin/dashboard/_head.html.haml b/app/views/admin/dashboard/_head.html.haml
index 7b3f88c24df..b74da64f82e 100644
--- a/app/views/admin/dashboard/_head.html.haml
+++ b/app/views/admin/dashboard/_head.html.haml
@@ -20,3 +20,7 @@
= 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/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 5b8a0262ea0..50770465f07 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -88,28 +88,17 @@
= select_tag :access_level, options_for_select(GroupMember.access_level_roles), class: "project-access-select select2"
%hr
= button_tag 'Add users to group', class: "btn btn-create"
+
+ = render 'shared/members/requests', membership_source: @group, members: @members.request
+
.panel.panel-default
.panel-heading
- %h3.panel-title
- Members
- %span.badge
- #{@group.group_members.count}
- %ul.well-list.group-users-list
- - @members.each do |member|
- - user = member.user
- %li{class: dom_class(member), id: (dom_id(user) if user)}
- .list-item-name
- - if user
- %strong
- = link_to user.name, admin_user_path(user)
- - else
- %strong
- = member.invite_email
- (invited)
- %span.pull-right.light
- = member.human_access
- - if can?(current_user, :destroy_group_member, member)
- = link_to group_group_member_path(@group, member), data: { confirm: remove_member_message(member) }, method: :delete, remote: true, class: "btn-xs btn btn-remove", title: 'Remove user from group' do
- %i.fa.fa-minus.fa-inverse
+ %strong= @group.name
+ group members
+ %span.badge= @group.members.non_request.size
+ .pull-right
+ = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@group, :members]), class: "btn btn-xs"
+ %ul.well-list.group-users-list.content-list
+ = render partial: 'shared/members/member', collection: @members.non_request, as: :member, locals: { show_controls: false }
.panel-footer
- = paginate @members, param_name: 'members_page', theme: 'gitlab'
+ = paginate @members.non_request, param_name: 'members_page', theme: 'gitlab'
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 9e55a562e18..461d588415d 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -135,44 +135,27 @@
- if @group
.panel.panel-default
.panel-heading
- %strong #{@group.name}
- group members (#{@group.group_members.count})
+ %strong= @group.name
+ group members
+ %span.badge= @group_members.non_request.size
.pull-right
= link_to admin_group_path(@group), class: 'btn btn-xs' do
- %i.fa.fa-pencil-square-o
- %ul.well-list
- - @group_members.each do |member|
- = render 'shared/members/member', member: member, show_controls: false
+ = icon('pencil-square-o', text: 'Manage Access')
+ %ul.well-list.content-list
+ = render partial: 'shared/members/member', collection: @group_members.non_request, as: :member, locals: { show_controls: false }
.panel-footer
- = paginate @group_members, param_name: 'group_members_page', theme: 'gitlab'
+ = paginate @group_members.non_request, param_name: 'group_members_page', theme: 'gitlab'
+
+ = render 'shared/members/requests', membership_source: @project, members: @project_members.request
.panel.panel-default
.panel-heading
- Project members
- %small
- (#{@project.users.count})
+ %strong= @project.name
+ project members
+ %span.badge= @project.users.size
.pull-right
- = link_to namespace_project_project_members_path(@project.namespace, @project), class: "btn btn-xs" do
- %i.fa.fa-pencil-square-o
- Manage Access
- %ul.well-list.project_members
- - @project_members.each do |project_member|
- - user = project_member.user
- %li.project_member
- .list-item-name
- - if user
- %strong
- = link_to user.name, admin_user_path(user)
- - else
- %strong
- = project_member.invite_email
- (invited)
- .pull-right
- - if project_member.owner?
- %span.light Owner
- - else
- %span.light= project_member.human_access
- = link_to namespace_project_project_member_path(@project.namespace, @project, project_member), data: { confirm: remove_member_message(project_member)}, method: :delete, remote: true, class: "btn btn-sm btn-remove" do
- %i.fa.fa-times
+ = link_to icon('pencil-square-o', text: 'Manage Access'), polymorphic_url([@project, :members]), class: "btn btn-xs"
+ %ul.well-list.project_members.content-list
+ = render partial: 'shared/members/member', collection: @project_members.non_request, as: :member, locals: { show_controls: false }
.panel-footer
- = paginate @project_members, param_name: 'project_members_page', theme: 'gitlab'
+ = paginate @project_members.non_request, param_name: 'project_members_page', theme: 'gitlab'
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 2dad64b8d0f..5eff77aff2d 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -1,67 +1,73 @@
-%p.lead.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.
- Registration token is
- %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
+- @no_container = true
+= render "admin/dashboard/head"
-.bs-callout.clearfix
- .pull-left
- %p
- You can reset runners registration token by pressing a button below.
- %p
- = button_to reset_runners_token_admin_application_settings_path,
- method: :put, class: 'btn btn-default',
- data: { confirm: 'Are you sure you want to reset registration token?' } do
- = icon('refresh')
- Reset runners registration token
+%div{ class: (container_class) }
+
+ %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.
+ %br
+ Registration token is
+ %code{ id: 'runners-token' } #{current_application_settings.runners_registration_token}
-.bs-callout
- %p
- 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.
- %br
+ .bs-callout.clearfix
+ .pull-left
+ %p
+ You can reset runners registration token by pressing a button below.
+ %p
+ = button_to reset_runners_token_admin_application_settings_path,
+ method: :put, class: 'btn btn-default',
+ data: { confirm: 'Are you sure you want to reset registration token?' } do
+ = icon('refresh')
+ Reset runners registration token
+
+ .bs-callout
+ %p
+ 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.
+ %br
- %div
- %span Each runner can be in one of the following states:
- %ul
- %li
- %span.label.label-success shared
- \- run builds from all unassigned projects
- %li
- %span.label.label-info specific
- \- run builds from assigned projects
- %li
- %span.label.label-danger paused
- \- runner will not receive any new builds
+ %div
+ %span Each runner can be in one of the following states:
+ %ul
+ %li
+ %span.label.label-success shared
+ \- run builds from all unassigned projects
+ %li
+ %span.label.label-info specific
+ \- run builds from assigned projects
+ %li
+ %span.label.label-danger paused
+ \- runner will not receive any new builds
-.append-bottom-20.clearfix
- .pull-left
- = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
- .form-group
- = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
- = submit_tag 'Search', class: 'btn'
+ .append-bottom-20.clearfix
+ .pull-left
+ = form_tag admin_runners_path, id: 'runners-search', class: 'form-inline', method: :get do
+ .form-group
+ = search_field_tag :search, params[:search], class: 'form-control', placeholder: 'Runner description or token', spellcheck: false
+ = submit_tag 'Search', class: 'btn'
- .pull-right.light
- Runners with last contact less than a minute ago: #{@active_runners_cnt}
+ .pull-right.light
+ Runners with last contact less than a minute ago: #{@active_runners_cnt}
-%br
+ %br
-.table-holder
- %table.table
- %thead
- %tr
- %th Type
- %th Runner token
- %th Description
- %th Projects
- %th Builds
- %th Tags
- %th Last contact
- %th
+ .table-holder
+ %table.table
+ %thead
+ %tr
+ %th Type
+ %th Runner token
+ %th Description
+ %th Projects
+ %th Builds
+ %th Tags
+ %th Last contact
+ %th
- - @runners.each do |runner|
- = render "admin/runners/runner", runner: runner
-= paginate @runners
+ - @runners.each do |runner|
+ = render "admin/runners/runner", runner: runner
+ = paginate @runners
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index e049b40bfab..61abfc6ecbe 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -28,7 +28,7 @@
.col-md-6
%h4 Restrict projects for this runner
- if @runner.projects.any?
- %table.table
+ %table.table.assigned-projects
%thead
%tr
%th Assigned projects
@@ -44,7 +44,7 @@
.pull-right
= link_to 'Disable', [:admin, project.namespace.becomes(Namespace), project, runner_project], method: :delete, class: 'btn btn-danger btn-xs'
- %table.table
+ %table.table.unassigned-projects
%thead
%tr
%th Project
diff --git a/app/views/dashboard/_projects_head.html.haml b/app/views/dashboard/_projects_head.html.haml
index d35f332e1e0..f7abad54286 100644
--- a/app/views/dashboard/_projects_head.html.haml
+++ b/app/views/dashboard/_projects_head.html.haml
@@ -13,7 +13,7 @@
Explore Projects
.nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, params[:filter_projects], placeholder: 'Filter by name...', class: 'project-filter-form-field form-control input-short projects-list-filter', spellcheck: false, id: 'project-filter-form-field', tabindex: "2"
= render 'shared/projects/dropdown'
- if current_user.can_create_project?
diff --git a/app/views/groups/group_members/index.html.haml b/app/views/groups/group_members/index.html.haml
index a36531e095a..d6acade84f1 100644
--- a/app/views/groups/group_members/index.html.haml
+++ b/app/views/groups/group_members/index.html.haml
@@ -17,8 +17,7 @@
.panel-heading
%strong #{@group.name}
group members
- %small
- (#{@members.total_count})
+ %span.badge= @members.non_request.size
.controls
= form_tag group_group_members_path(@group), method: :get, class: 'form-inline member-search-form' do
.form-group
diff --git a/app/views/groups/show.html.haml b/app/views/groups/show.html.haml
index 62ebd69485c..aecefbc6e8f 100644
--- a/app/views/groups/show.html.haml
+++ b/app/views/groups/show.html.haml
@@ -33,7 +33,7 @@
= link_to "#shared", 'data-toggle' => 'tab' do
Shared Projects
.nav-controls
- = form_tag request.original_url, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
+ = form_tag request.path, method: :get, class: 'project-filter-form', id: 'project-filter-form' do |f|
= search_field_tag :filter_projects, nil, placeholder: 'Filter by name', class: 'projects-list-filter form-control', spellcheck: false
= render 'shared/projects/dropdown'
- if can? current_user, :create_projects, @group
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index 01648047ce2..8cc0b59edeb 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -28,8 +28,12 @@
.key &#8984; shift p
- else
.key ctrl shift p
-
%td Toggle Markdown preview
+ %tr
+ %td.shortcut
+ .key
+ %i.fa.fa-arrow-up
+ %td Edit last comment (when focused on an empty textarea)
%tbody
%tr
%th
diff --git a/app/views/import/github/status.html.haml b/app/views/import/github/status.html.haml
index 6c4a9d68d1f..7486b1423e2 100644
--- a/app/views/import/github/status.html.haml
+++ b/app/views/import/github/status.html.haml
@@ -6,7 +6,7 @@
%p
%i.fa.fa-warning
- To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process.
+ To import GitHub pull requests, any pull request source branches that had been deleted are temporarily restored on GitHub. To prevent any connected CI services from being overloaded with dozens of irrelevant branches being created and deleted again, GitHub webhooks are temporarily disabled during the import process, but only if you have admin access to the GitHub repository.
%p.light
Select projects you want to import.
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 199ab3c38c3..2234bf79c87 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -13,7 +13,7 @@
= image_tag avatar_icon(current_user, 60), alt: 'Profile', class: 'avatar avatar s36'
.username
= current_user.username
- = link_to '#', class: "nav-header-btn text-center pin-nav-btn #{'is-active' if pinned_nav?} js-nav-pin", title: 'Pin/Unpin navigation' do
+ = link_to '#', class: "nav-header-btn text-center 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
%span.sr-only Toggle navigation pinning
= icon('thumb-tack')
- if defined?(nav) && nav
diff --git a/app/views/layouts/nav/_admin.html.haml b/app/views/layouts/nav/_admin.html.haml
index 54aa34bee0b..66e5ec1ad1a 100644
--- a/app/views/layouts/nav/_admin.html.haml
+++ b/app/views/layouts/nav/_admin.html.haml
@@ -1,64 +1,41 @@
-%ul.nav-links.scrolling-tabs
- .fade-left
- = nav_link(controller: %w(dashboard admin projects users groups builds), html_options: {class: 'home'}) do
- = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
- %span
- Overview
- = nav_link(controller: %w(background_jobs logs health_check)) do
- = link_to admin_background_jobs_path, title: 'Monitoring' do
- %span
- Monitoring
- = nav_link(controller: :deploy_keys) do
- = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
- %span
- Deploy Keys
- = nav_link path: ['runners#index', 'runners#show'] do
- = link_to admin_runners_path, title: 'Runners' do
- %span
- Runners
- = nav_link(controller: :broadcast_messages) do
- = link_to admin_broadcast_messages_path, title: 'Messages' do
- %span
- Messages
- = nav_link(controller: :hooks) do
- = link_to admin_hooks_path, title: 'Hooks' do
- %span
- Hooks
+%div{ class: nav_control_class }
+ = render 'layouts/nav/admin_settings'
- = nav_link(controller: :appearances) do
- = link_to admin_appearances_path, title: 'Appearances' do
- %span
- Appearance
-
- = nav_link(controller: :applications) do
- = link_to admin_applications_path, title: 'Applications' do
- %span
- Applications
-
- = nav_link(controller: :services) do
- = link_to admin_application_settings_services_path, title: 'Service Templates' do
- %span
- Service Templates
-
- = nav_link(controller: :labels) do
- = link_to admin_labels_path, title: 'Labels' do
- %span
- Labels
+ %ul.nav-links.scrolling-tabs
+ %li.fade-left
+ = icon('arrow-left')
+ = nav_link(controller: %w(dashboard admin projects users groups builds runners), html_options: {class: 'home'}) do
+ = link_to admin_root_path, title: 'Overview', class: 'shortcuts-tree' do
+ %span
+ Overview
+ = nav_link(controller: %w(background_jobs logs health_check)) do
+ = link_to admin_background_jobs_path, title: 'Monitoring' do
+ %span
+ Monitoring
+ = nav_link(controller: :broadcast_messages) do
+ = link_to admin_broadcast_messages_path, title: 'Messages' do
+ %span
+ Messages
+ = nav_link(controller: :hooks) do
+ = link_to admin_hooks_path, title: 'Hooks' do
+ %span
+ System Hooks
- = nav_link(controller: :abuse_reports) do
- = link_to admin_abuse_reports_path, title: "Abuse Reports" do
- %span
- Abuse Reports
- %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
+ = nav_link(controller: :applications) do
+ = link_to admin_applications_path, title: 'Applications' do
+ %span
+ Applications
- - if askimet_enabled?
- = nav_link(controller: :spam_logs) do
- = link_to admin_spam_logs_path, title: "Spam Logs" do
+ = nav_link(controller: :abuse_reports) do
+ = link_to admin_abuse_reports_path, title: "Abuse Reports" do
%span
- Spam Logs
+ Abuse Reports
+ %span.badge.count= number_with_delimiter(AbuseReport.count(:all))
- = nav_link(controller: :application_settings, html_options: { class: 'separate-item'}) do
- = link_to admin_application_settings_path, title: 'Settings' do
- %span
- Settings
- .fade-right
+ - if askimet_enabled?
+ = nav_link(controller: :spam_logs) do
+ = link_to admin_spam_logs_path, title: "Spam Logs" do
+ %span
+ Spam Logs
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_admin_settings.html.haml b/app/views/layouts/nav/_admin_settings.html.haml
new file mode 100644
index 00000000000..38e9b80d129
--- /dev/null
+++ b/app/views/layouts/nav/_admin_settings.html.haml
@@ -0,0 +1,31 @@
+.controls
+ .dropdown.admin-settings-dropdown
+ %a.dropdown-new.btn.btn-default{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ = nav_link(controller: :deploy_keys) do
+ = link_to admin_deploy_keys_path, title: 'Deploy Keys' do
+ %span
+ Deploy Keys
+
+ = nav_link(controller: :services) do
+ = link_to admin_application_settings_services_path, title: 'Service Templates' do
+ %span
+ Service Templates
+
+ = nav_link(controller: :labels) do
+ = link_to admin_labels_path, title: 'Labels' do
+ %span
+ Labels
+
+ = nav_link(controller: :appearances) do
+ = link_to admin_appearances_path, title: 'Appearances' do
+ %span
+ Appearance
+
+ %li.divider
+ = nav_link(controller: :application_settings) do
+ = link_to admin_application_settings_path, title: 'Settings' do
+ %span
+ Settings
diff --git a/app/views/layouts/nav/_group.html.haml b/app/views/layouts/nav/_group.html.haml
index 66361a644dd..f7aa9fab7cf 100644
--- a/app/views/layouts/nav/_group.html.haml
+++ b/app/views/layouts/nav/_group.html.haml
@@ -2,7 +2,8 @@
= render 'layouts/nav/group_settings'
%ul.nav-links.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(path: 'groups#show', html_options: {class: 'home'}) do
= link_to group_path(@group), title: 'Home' do
%span
@@ -31,4 +32,5 @@
= link_to group_group_members_path(@group), title: 'Members' do
%span
Members
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_group_settings.html.haml b/app/views/layouts/nav/_group_settings.html.haml
index dac46648b9f..3a24b09ab7e 100644
--- a/app/views/layouts/nav/_group_settings.html.haml
+++ b/app/views/layouts/nav/_group_settings.html.haml
@@ -1,16 +1,22 @@
- if current_user
- - if access = @group.users.find_by(id: current_user.id)
- .controls
- .dropdown.group-settings-dropdown
- %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
- = icon('cog')
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-align-right
- - if can?(current_user, :admin_group, @group)
- = nav_link(path: 'groups#projects') do
- = link_to projects_group_path(@group), title: 'Projects' do
- Projects
- %li.divider
- %li
- = link_to edit_group_path(@group) do
- Edit Group
+ - can_edit = can?(current_user, :admin_group, @group)
+ - member = @group.members.non_request.find_by(user_id: current_user.id)
+ - can_leave = member && can?(current_user, :destroy_group_member, member)
+
+ .controls
+ .dropdown.group-settings-dropdown
+ %a.dropdown-new.btn.btn-default#group-settings-button{href: '#', 'data-toggle' => 'dropdown'}
+ = icon('cog')
+ = icon('caret-down')
+ %ul.dropdown-menu.dropdown-menu-align-right
+ = nav_link(path: 'groups#projects') do
+ = link_to 'Projects', projects_group_path(@group), title: 'Projects'
+ %li.divider
+ - if can_edit
+ %li
+ = link_to 'Edit Group', edit_group_path(@group)
+ - if can_leave
+ %li
+ = link_to polymorphic_path([:leave, @group, :members]),
+ data: { confirm: leave_confirmation_message(@group) }, method: :delete, title: 'Leave group' do
+ Leave Group
diff --git a/app/views/layouts/nav/_profile.html.haml b/app/views/layouts/nav/_profile.html.haml
index bb6f14a6225..44ea939b7e4 100644
--- a/app/views/layouts/nav/_profile.html.haml
+++ b/app/views/layouts/nav/_profile.html.haml
@@ -1,5 +1,6 @@
%ul.nav-links.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(path: 'profiles#show', html_options: {class: 'home'}) do
= link_to profile_path, title: 'Profile Settings' do
%span
@@ -43,4 +44,5 @@
= link_to audit_log_profile_path, title: 'Audit Log' do
%span
Audit Log
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index 39ea4920ccc..27e840df503 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -5,19 +5,20 @@
= icon('cog')
= icon('caret-down')
%ul.dropdown-menu.dropdown-menu-align-right
- - is_project_member = @project.users.exists?(current_user.id)
- - access = @project.team.max_member_access(current_user.id)
- can_edit = can?(current_user, :admin_project, @project)
+ -# We don't use @project.team.find_member because it searches for group members too...
+ - member = @project.members.non_request.find_by(user_id: current_user.id)
+ - can_leave = member && can?(current_user, :destroy_project_member, member)
- = render 'layouts/nav/project_settings', access: access, can_edit: can_edit
+ = render 'layouts/nav/project_settings', can_edit: can_edit
- - if can_edit || is_project_member
+ - if can_edit || can_leave
%li.divider
- if can_edit
%li
= link_to edit_project_path(@project) do
Edit Project
- - if is_project_member
+ - if can_leave
%li
= link_to polymorphic_path([:leave, @project, :members]),
data: { confirm: leave_confirmation_message(@project) }, method: :delete, title: 'Leave project' do
@@ -25,7 +26,8 @@
%div{ class: nav_control_class }
%ul.nav-links.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(path: 'projects#show', html_options: {class: 'home'}) do
= link_to project_path(@project), title: 'Project', class: 'shortcuts-project' do
%span
@@ -38,9 +40,9 @@
- if project_nav_tab? :files
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file commit commits compare repositories tags branches releases network)) do
- = link_to project_files_path(@project), title: 'Code', class: 'shortcuts-tree' do
+ = link_to project_files_path(@project), title: 'Repository', class: 'shortcuts-tree' do
%span
- Code
+ Repository
- if project_nav_tab? :pipelines
= nav_link(controller: [:pipelines, :builds, :environments]) do
@@ -109,4 +111,5 @@
%li.hidden
= link_to project_commits_path(@project), title: 'Commits', class: 'shortcuts-commits' do
Commits
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/layouts/nav/_project_settings.html.haml b/app/views/layouts/nav/_project_settings.html.haml
index 13d32bd1354..51a54b4f262 100644
--- a/app/views/layouts/nav/_project_settings.html.haml
+++ b/app/views/layouts/nav/_project_settings.html.haml
@@ -3,7 +3,7 @@
= link_to namespace_project_project_members_path(@project.namespace, @project), title: 'Members', class: 'team-tab tab' do
%span
Members
-- if access && can_edit
+- if can_edit
- if @project.allowed_to_share_with_group?
= nav_link(controller: :group_links) do
= link_to namespace_project_group_links_path(@project.namespace, @project), title: "Groups" do
diff --git a/app/views/notify/project_was_not_exported_email.html.haml b/app/views/notify/project_was_not_exported_email.html.haml
index c9e9ade2cf1..c888da29c17 100644
--- a/app/views/notify/project_was_not_exported_email.html.haml
+++ b/app/views/notify/project_was_not_exported_email.html.haml
@@ -6,4 +6,4 @@
%ul
- @errors.each do |error|
%li
- error
+ #{error}
diff --git a/app/views/notify/project_was_not_exported_email.text.erb b/app/views/notify/project_was_not_exported_email.text.erb
deleted file mode 100644
index a07f6edacf7..00000000000
--- a/app/views/notify/project_was_not_exported_email.text.erb
+++ /dev/null
@@ -1,6 +0,0 @@
-Project <%= @project.name %> couldn't be exported.
-
-The errors we encountered were:
-
-- @errors.each do |error|
-<%= error %> \ No newline at end of file
diff --git a/app/views/notify/project_was_not_exported_email.text.haml b/app/views/notify/project_was_not_exported_email.text.haml
new file mode 100644
index 00000000000..b27cb620b9e
--- /dev/null
+++ b/app/views/notify/project_was_not_exported_email.text.haml
@@ -0,0 +1,6 @@
+= "Project #{@project.name} couldn't be exported."
+
+= "The errors we encountered were:"
+
+- @errors.each do |error|
+ #{error} \ No newline at end of file
diff --git a/app/views/profiles/notifications/_group_settings.html.haml b/app/views/profiles/notifications/_group_settings.html.haml
index f0cf82afe83..537bba21f4a 100644
--- a/app/views/profiles/notifications/_group_settings.html.haml
+++ b/app/views/profiles/notifications/_group_settings.html.haml
@@ -9,5 +9,4 @@
= link_to group.name, group_path(group)
.pull-right
- = form_for [group, setting], remote: true, html: { class: 'update-notifications' } do |f|
- = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/_project_settings.html.haml b/app/views/profiles/notifications/_project_settings.html.haml
index e0fad555c09..5b2a69b8891 100644
--- a/app/views/profiles/notifications/_project_settings.html.haml
+++ b/app/views/profiles/notifications/_project_settings.html.haml
@@ -9,5 +9,4 @@
= link_to_project(project)
.pull-right
- = form_for [project.namespace.becomes(Namespace), project, setting], remote: true, html: { class: 'update-notifications' } do |f|
- = f.select :level, NotificationSetting.levels.keys, {}, class: 'form-control trigger-submit'
+ = render 'shared/notifications/button', notification_setting: setting
diff --git a/app/views/profiles/notifications/show.html.haml b/app/views/profiles/notifications/show.html.haml
index f2659ac14b5..f77738f97f5 100644
--- a/app/views/profiles/notifications/show.html.haml
+++ b/app/views/profiles/notifications/show.html.haml
@@ -24,12 +24,15 @@
.form-group
= f.label :notification_email, class: "label-light"
= f.select :notification_email, @user.all_emails, { include_blank: false }, class: "select2"
- .form-group
- = f.label :notification_level, class: 'label-light'
- = notification_level_radio_buttons
- .prepend-top-default
- = f.submit 'Update settings', class: "btn btn-create"
+ = label_tag :global_notification_level, "Global notification level", class: "label-light"
+ %br
+ .clearfix
+ .form-group.pull-left.global-notification-setting
+ = render 'shared/notifications/button', notification_setting: @global_notification_setting, left_align: true
+
+ .clearfix
+
%hr
%h5
Groups (#{@group_notifications.count})
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index 2b19ee93eea..86ea08dd229 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -21,7 +21,7 @@
= link_to project_path(forked_from_project) do
= forked_from_project.namespace.try(:name)
- .project-repo-buttons
+ .project-repo-buttons.project-action-buttons
.count-buttons
= render 'projects/buttons/star'
= render 'projects/buttons/fork'
@@ -29,13 +29,13 @@
.project-clone-holder
= render "shared/clone_panel"
- .project-repo-buttons.project-right-buttons
+ .project-repo-buttons.btn-group.project-right-buttons
- if current_user
= render 'shared/members/access_request_buttons', source: @project
- .btn-group
- = render "projects/buttons/download"
- = render 'projects/buttons/dropdown'
- = render 'projects/buttons/notifications'
+
+ = render "projects/buttons/download"
+ = render 'projects/buttons/dropdown'
+ = render 'shared/notifications/button', notification_setting: @notification_setting
:javascript
new Star();
diff --git a/app/views/projects/_md_preview.html.haml b/app/views/projects/_md_preview.html.haml
index 28a28282fd3..ca6714ef42b 100644
--- a/app/views/projects/_md_preview.html.haml
+++ b/app/views/projects/_md_preview.html.haml
@@ -14,8 +14,17 @@
%span This is a confidential issue. Your comment will not be visible to the public.
%li.pull-right
- %button.zen-control.zen-control-full.js-zen-enter{ type: 'button', tabindex: -1 }
- Go full screen
+ .toolbar-group
+ = markdown_toolbar_button({icon: "bold fw", data: { "md-tag" => "**" }, title: "Add bold text" })
+ = markdown_toolbar_button({icon: "italic fw", data: { "md-tag" => "*" }, title: "Add italic text" })
+ = markdown_toolbar_button({icon: "quote-right fw", data: { "md-tag" => "> ", "md-prepend" => true }, title: "Insert a quote" })
+ = markdown_toolbar_button({icon: "code fw", data: { "md-tag" => "`" }, title: "Insert code" })
+ = markdown_toolbar_button({icon: "list-ul fw", data: { "md-tag" => "* ", "md-prepend" => true }, title: "Add a bullet list" })
+ = markdown_toolbar_button({icon: "list-ol fw", data: { "md-tag" => "1. ", "md-prepend" => true }, title: "Add a numbered list" })
+ = markdown_toolbar_button({icon: "check-square-o fw", data: { "md-tag" => "* [ ] ", "md-prepend" => true }, title: "Add a task list" })
+ .toolbar-group
+ %button.toolbar-btn.js-zen-enter.has-tooltip.hidden-xs{ type: "button", tabindex: -1, aria: { label: "Go full screen" }, title: "Go full screen", data: { container: "body" } }
+ =icon("arrows-alt fw")
.md-write-holder
= yield
@@ -24,7 +33,7 @@
- if defined?(referenced_users) && referenced_users
%div.referenced-users.hide
%span
- = icon('exclamation-triangle')
+ = icon("exclamation-triangle")
You are about to add
%strong
%span.js-referenced-users-count 0
diff --git a/app/views/projects/badges/index.html.haml b/app/views/projects/badges/index.html.haml
index ee63bc55a30..ac80951dd4f 100644
--- a/app/views/projects/badges/index.html.haml
+++ b/app/views/projects/badges/index.html.haml
@@ -7,7 +7,7 @@
%b Builds badge &middot;
= @build_badge.to_html
.pull-right
- = render 'shared/ref_switcher', destination: 'badges'
+ = render 'shared/ref_switcher', destination: 'badges', align_right: true
.panel-body
.row
.col-md-2.text-center
diff --git a/app/views/projects/blob/_editor.html.haml b/app/views/projects/blob/_editor.html.haml
index ae89637df60..29c7d45074a 100644
--- a/app/views/projects/blob/_editor.html.haml
+++ b/app/views/projects/blob/_editor.html.haml
@@ -17,6 +17,8 @@
= dropdown_tag("Choose a License template", options: { toggle_class: 'js-license-selector', title: "Choose a license", filter: true, placeholder: "Filter", data: { data: licenses_for_select, project: @project.name, fullname: @project.namespace.human_name } } )
.gitignore-selector.js-gitignore-selector-wrap.hidden
= 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 } } )
.encoding-selector
= select_tag :encoding, options_for_select([ "base64", "text" ], "text"), class: 'select2'
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 87c732626a6..4bd85061240 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -20,15 +20,15 @@
protected
.controls.hidden-xs
- if create_mr_button?(@repository.root_ref, branch.name)
- = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-grouped btn-xs' do
+ = link_to create_mr_path(@repository.root_ref, branch.name), class: 'btn btn-default' do
Merge Request
- if branch.name != @repository.root_ref
- = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-grouped btn-xs', method: :post, title: "Compare" do
+ = link_to namespace_project_compare_index_path(@project.namespace, @project, from: @repository.root_ref, to: branch.name), class: 'btn btn-default', method: :post, title: "Compare" do
Compare
- if can_remove_branch?(@project, branch.name)
- = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-grouped btn-xs btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
+ = link_to namespace_project_branch_path(@project.namespace, @project, branch.name), class: 'btn btn-remove remove-row has-tooltip', title: "Delete branch", method: :delete, data: { confirm: "Deleting the '#{branch.name}' branch cannot be undone. Are you sure?", container: 'body' }, remote: true do
= icon("trash-o")
- if branch.name != @repository.root_ref
diff --git a/app/views/projects/buttons/_notifications.html.haml b/app/views/projects/buttons/_notifications.html.haml
deleted file mode 100644
index a7a97181096..00000000000
--- a/app/views/projects/buttons/_notifications.html.haml
+++ /dev/null
@@ -1,11 +0,0 @@
-- if @notification_setting
- = form_for @notification_setting, url: namespace_project_notification_setting_path(@project.namespace.becomes(Namespace), @project), method: :patch, remote: true, html: { class: 'inline', id: 'notification-form' } do |f|
- = f.hidden_field :level
- .dropdown.hidden-sm
- %button.btn.btn-default.notifications-btn#notifications-button{ data: { toggle: "dropdown" }, aria: { haspopup: "true", expanded: "false" } }
- = icon('bell')
- = notification_title(@notification_setting.level)
- = icon('caret-down')
- %ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-align-right.dropdown-menu-selectable.dropdown-menu-large{ role: "menu" }
- - NotificationSetting.levels.each do |level|
- = notification_list_item(level.first, @notification_setting)
diff --git a/app/views/projects/ci/pipelines/_pipeline.html.haml b/app/views/projects/ci/pipelines/_pipeline.html.haml
index b8d8758fd2b..e38d1ff5ff0 100644
--- a/app/views/projects/ci/pipelines/_pipeline.html.haml
+++ b/app/views/projects/ci/pipelines/_pipeline.html.haml
@@ -24,8 +24,8 @@
%span.label.label-warning stuck
%p.commit-title
- - if commit_data = pipeline.commit_data
- = link_to_gfm truncate(commit_data.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit_data.id), class: "commit-row-message"
+ - if commit = pipeline.commit
+ = link_to_gfm truncate(commit.title, length: 60), namespace_project_commit_path(@project.namespace, @project, commit.id), class: "commit-row-message"
- else
Cant find HEAD commit for this branch
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index a959b34a539..929496f81d8 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -10,29 +10,30 @@
= cache(cache_key) do
%li.commit.js-toggle-container{ id: "commit-#{commit.short_id}" }
= commit_author_avatar(commit, size: 36)
- .commit-row-title
- %span.item-title
- = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
- %span.commit-row-message.visible-xs-inline
- &middot;
- = commit.short_id
- - if commit.status
- = render_commit_status(commit, cssclass: 'visible-xs-inline')
- - if commit.description?
- %a.text-expander.hidden-xs.js-toggle-button ...
+ .commit-info-block
+ .commit-row-title
+ %span.item-title
+ = link_to_gfm commit.title, namespace_project_commit_path(project.namespace, project, commit.id), class: "commit-row-message"
+ %span.commit-row-message.visible-xs-inline
+ &middot;
+ = commit.short_id
+ - if commit.status
+ = render_commit_status(commit, cssclass: 'visible-xs-inline')
+ - if commit.description?
+ %a.text-expander.hidden-xs.js-toggle-button ...
- .commit-actions.hidden-xs
- - if commit.status
- = render_commit_status(commit, cssclass: 'btn btn-transparent')
- = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent')
- = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
- = link_to_browse_code(project, commit)
+ .commit-actions.hidden-xs
+ - if commit.status
+ = render_commit_status(commit, cssclass: 'btn btn-transparent')
+ = clipboard_button_with_class({ clipboard_text: commit.id }, css_class: 'btn-transparent')
+ = link_to commit.short_id, namespace_project_commit_path(project.namespace, project, commit), class: "commit-short-id btn btn-transparent"
+ = link_to_browse_code(project, commit)
- - if commit.description?
- %pre.commit-row-description.js-toggle-content
- = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
+ - if commit.description?
+ %pre.commit-row-description.js-toggle-content
+ = preserve(markdown(escape_once(commit.description), pipeline: :single_line, author: commit.author))
- .commit-row-info
- = commit_author_link(commit, avatar: false, size: 24)
- authored
- #{time_ago_with_tooltip(commit.committed_date)}
+ .commit-row-info
+ = commit_author_link(commit, avatar: false, size: 24)
+ authored
+ #{time_ago_with_tooltip(commit.committed_date)}
diff --git a/app/views/projects/commits/_head.html.haml b/app/views/projects/commits/_head.html.haml
index c8aa849c217..54dab4bff07 100644
--- a/app/views/projects/commits/_head.html.haml
+++ b/app/views/projects/commits/_head.html.haml
@@ -1,7 +1,8 @@
.scrolling-tabs-container
.nav-links.sub-nav.scrolling-tabs
%ul{ class: (container_class) }
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= nav_link(controller: %w(tree blob blame edit_tree new_tree find_file)) do
= link_to project_files_path(@project) do
Files
@@ -25,4 +26,5 @@
= nav_link(controller: [:tags, :releases]) do
= link_to namespace_project_tags_path(@project.namespace, @project) do
Tags
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/projects/compare/index.html.haml b/app/views/projects/compare/index.html.haml
index c322942aeba..b22285c11e0 100644
--- a/app/views/projects/compare/index.html.haml
+++ b/app/views/projects/compare/index.html.haml
@@ -3,7 +3,7 @@
= render "projects/commits/head"
%div{ class: (container_class) }
- .row-content-block.second-block.content-component-block
+ .sub-header-block
Compare branches, tags or commit ranges.
%br
Fill input field with commit id like
diff --git a/app/views/projects/compare/show.html.haml b/app/views/projects/compare/show.html.haml
index cdc34f51d6d..f4ec7b767f6 100644
--- a/app/views/projects/compare/show.html.haml
+++ b/app/views/projects/compare/show.html.haml
@@ -1,24 +1,24 @@
+- @no_container = true
- page_title "#{params[:from]}...#{params[:to]}"
= render "projects/commits/head"
+%div{ class: (container_class) }
+ .sub-header-block.no-bottom-space
+ = render "form"
-.row-content-block
- = render "form"
-
-- if @commits.present?
- .prepend-top-default
+ - if @commits.present?
= render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs
-- else
- .light-well.prepend-top-default
- .center
- %h4
- There isn't anything to compare.
- %p.slead
- - if params[:to] == params[:from]
- %span.label-branch #{params[:from]}
- and
- %span.label-branch #{params[:to]}
- are the same.
- - else
- You'll need to use different branch names to get a valid comparison.
+ - else
+ .light-well
+ .center
+ %h4
+ There isn't anything to compare.
+ %p.slead
+ - if params[:to] == params[:from]
+ %span.label-branch #{params[:from]}
+ and
+ %span.label-branch #{params[:to]}
+ are the same.
+ - else
+ You'll need to use different branch names to get a valid comparison.
diff --git a/app/views/projects/container_registry/_tag.html.haml b/app/views/projects/container_registry/_tag.html.haml
index f35faa6afb5..10822b6184c 100644
--- a/app/views/projects/container_registry/_tag.html.haml
+++ b/app/views/projects/container_registry/_tag.html.haml
@@ -3,9 +3,9 @@
= escape_once(tag.name)
= clipboard_button(clipboard_text: "docker pull #{tag.path}")
%td
- - if layer = tag.layers.first
- %span.has-tooltip{ title: "#{layer.revision}" }
- = layer.short_revision
+ - if tag.revision
+ %span.has-tooltip{ title: "#{tag.revision}" }
+ = tag.short_revision
- else
\-
%td
diff --git a/app/views/projects/environments/index.html.haml b/app/views/projects/environments/index.html.haml
index ae9e77e7d89..a03f117291f 100644
--- a/app/views/projects/environments/index.html.haml
+++ b/app/views/projects/environments/index.html.haml
@@ -3,16 +3,24 @@
= render "projects/pipelines/head"
%div{ class: (container_class) }
- - if can?(current_user, :create_environment, @project)
+ - if can?(current_user, :create_environment, @project) && !@environments.blank?
.top-area
.nav-controls
= link_to new_namespace_project_environment_path(@project.namespace, @project), class: 'btn btn-create' do
New environment
- if @environments.blank?
- %ul.content-list.environments
- %li.nothing-here-block
- No environments to show
+ .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.environments
diff --git a/app/views/projects/environments/new.html.haml b/app/views/projects/environments/new.html.haml
index 54465828ba9..da325efecd2 100644
--- a/app/views/projects/environments/new.html.haml
+++ b/app/views/projects/environments/new.html.haml
@@ -4,6 +4,9 @@
.col-lg-3
%h4.prepend-top-0
New Environment
- %p Environments allow you to track deployments of your application
+ %p
+ Environments allow you to track deployments of your application
+ = succeed "." do
+ = link_to "Read more about environments", help_page_path("ci", "environments")
= render 'form'
diff --git a/app/views/projects/environments/show.html.haml b/app/views/projects/environments/show.html.haml
index 069b77b5adf..4c15e2759d6 100644
--- a/app/views/projects/environments/show.html.haml
+++ b/app/views/projects/environments/show.html.haml
@@ -13,10 +13,14 @@
= 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?
- %ul.content-list.environments
- %li.nothing-here-block
- No deployments for
- %strong= @environment.name
+ .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.environments
diff --git a/app/views/projects/forks/index.html.haml b/app/views/projects/forks/index.html.haml
index 4bcf2d9d533..dbe9ddfde2f 100644
--- a/app/views/projects/forks/index.html.haml
+++ b/app/views/projects/forks/index.html.haml
@@ -40,9 +40,3 @@
= render 'projects', projects: @forks
-
-- if @private_forks_count > 0
- .private-forks-notice
- = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
- %strong= pluralize(@private_forks_count, 'private fork')
- %span you have no access to.
diff --git a/app/views/projects/graphs/_head.html.haml b/app/views/projects/graphs/_head.html.haml
index 8becaea246f..a388d9a0a61 100644
--- a/app/views/projects/graphs/_head.html.haml
+++ b/app/views/projects/graphs/_head.html.haml
@@ -1,12 +1,14 @@
-- page_specific_javascripts asset_path("graphs/application.js")
-%ul.nav-links
- = 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.builds_enabled?
- = nav_link(action: :ci) do
- = link_to ci_namespace_project_graph_path do
- Continuous Integration
+.nav-links.sub-nav
+ %ul{ class: (container_class) }
+
+ - page_specific_javascripts asset_path("graphs/application.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.builds_enabled?
+ = nav_link(action: :ci) do
+ = link_to ci_namespace_project_graph_path do
+ Continuous Integration
diff --git a/app/views/projects/graphs/ci.html.haml b/app/views/projects/graphs/ci.html.haml
index 19ccc125ea8..e695d3ae369 100644
--- a/app/views/projects/graphs/ci.html.haml
+++ b/app/views/projects/graphs/ci.html.haml
@@ -1,15 +1,18 @@
+- @no_container = true
- page_title "Continuous Integration", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .oneline
- A collection of graphs for Continuous Integration
-#charts.ci-charts
- .row
- .col-md-6
- = render 'projects/graphs/ci/overall'
- .col-md-6
- = render 'projects/graphs/ci/build_times'
+%div{ class: (container_class) }
+ .sub-header-block
+ .oneline
+ A collection of graphs for Continuous Integration
- %hr
- = render 'projects/graphs/ci/builds'
+ #charts.ci-charts
+ .row
+ .col-md-6
+ = render 'projects/graphs/ci/overall'
+ .col-md-6
+ = render 'projects/graphs/ci/build_times'
+
+ %hr
+ = render 'projects/graphs/ci/builds'
diff --git a/app/views/projects/graphs/commits.html.haml b/app/views/projects/graphs/commits.html.haml
index d9b2fb6c065..0daffe68f6f 100644
--- a/app/views/projects/graphs/commits.html.haml
+++ b/app/views/projects/graphs/commits.html.haml
@@ -1,52 +1,54 @@
+- @no_container = true
- page_title "Commits", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs_commits'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
+%div{ class: (container_class) }
+ .sub-header-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs_commits'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
-%p.lead
- Commit statistics for
- %strong #{@ref}
- #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
+ %p.lead
+ Commit statistics for
+ %strong #{@ref}
+ #{@commits_graph.start_date.strftime('%b %d')} - #{@commits_graph.end_date.strftime('%b %d')}
-.row
- .col-md-6
- %ul
- %li
- %p.lead
- %strong #{@commits_graph.commits.size}
- commits during
- %strong #{@commits_graph.duration}
- days
- %li
- %p.lead
- Average
- %strong #{@commits_graph.commit_per_day}
- commits per day
- %li
- %p.lead
- Contributed by
- %strong #{@commits_graph.authors}
- authors
- .col-md-6
- %div
- %p.slead
- Commits per day of month
- %canvas#month-chart
-.row
- .col-md-6
- %div
- %p.slead
- Commits per day hour (UTC)
- %canvas#hour-chart
- .col-md-6
- %div
- %p.slead
- Commits per weekday
- %canvas#weekday-chart
+ .row
+ .col-md-6
+ %ul
+ %li
+ %p.lead
+ %strong #{@commits_graph.commits.size}
+ commits during
+ %strong #{@commits_graph.duration}
+ days
+ %li
+ %p.lead
+ Average
+ %strong #{@commits_graph.commit_per_day}
+ commits per day
+ %li
+ %p.lead
+ Contributed by
+ %strong #{@commits_graph.authors}
+ authors
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day of month
+ %canvas#month-chart
+ .row
+ .col-md-6
+ %div
+ %p.slead
+ Commits per day hour (UTC)
+ %canvas#hour-chart
+ .col-md-6
+ %div
+ %p.slead
+ Commits per weekday
+ %canvas#weekday-chart
:javascript
var responsiveChart = function (selector, data) {
diff --git a/app/views/projects/graphs/languages.html.haml b/app/views/projects/graphs/languages.html.haml
index 249c16f4709..6d97f552a8e 100644
--- a/app/views/projects/graphs/languages.html.haml
+++ b/app/views/projects/graphs/languages.html.haml
@@ -1,24 +1,26 @@
+- @no_container = true
- page_title "Languages", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .oneline
- Programming languages used in this repository
+%div{ class: (container_class) }
+ .sub-header-block
+ .oneline
+ Programming languages used in this repository
-.row
- .col-md-8
- %canvas#languages-chart{ height: 400 }
- .col-md-4
- %ul.bordered-list
- - @languages.each do |language|
- %li
- %span{ style: "color: #{language[:color]}" }
- = icon('circle')
- &nbsp;
- = language[:label]
- .pull-right
- = language[:value]
- \%
+ .row
+ .col-md-8
+ %canvas#languages-chart{ height: 400 }
+ .col-md-4
+ %ul.bordered-list
+ - @languages.each do |language|
+ %li
+ %span{ style: "color: #{language[:color]}" }
+ = icon('circle')
+ &nbsp;
+ = language[:label]
+ .pull-right
+ = language[:value]
+ \%
:javascript
var data = #{@languages.to_json};
diff --git a/app/views/projects/graphs/show.html.haml b/app/views/projects/graphs/show.html.haml
index 33970e7b909..9f7e2a361ff 100644
--- a/app/views/projects/graphs/show.html.haml
+++ b/app/views/projects/graphs/show.html.haml
@@ -1,29 +1,31 @@
+- @no_container = true
- page_title "Contributors", "Graphs"
= render 'head'
-.row-content-block.append-bottom-default
- .tree-ref-holder
- = render 'shared/ref_switcher', destination: 'graphs'
- %ul.breadcrumb.repo-breadcrumb
- = commits_breadcrumbs
-
-.loading-graph
- .center
- %h3.page-title
- %i.fa.fa-spinner.fa-spin
- Building repository graph.
- %p.slead Please wait a moment, this page will automatically refresh when ready.
-
-.stat-graph.hide
- .header.clearfix
- %h3#date_header.page-title
- %p.light
- Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
- %input#brush_change{:type => "hidden"}
- .graphs
- #contributors-master
- #contributors.clearfix
- %ol.contributors-list.clearfix
+%div{ class: (container_class) }
+ .sub-header-block
+ .tree-ref-holder
+ = render 'shared/ref_switcher', destination: 'graphs'
+ %ul.breadcrumb.repo-breadcrumb
+ = commits_breadcrumbs
+
+ .loading-graph
+ .center
+ %h3.page-title
+ %i.fa.fa-spinner.fa-spin
+ Building repository graph.
+ %p.slead Please wait a moment, this page will automatically refresh when ready.
+
+ .stat-graph.hide
+ .header.clearfix
+ %h3#date_header.page-title
+ %p.light
+ Commits to #{@ref}, excluding merge commits. Limited to 6,000 commits.
+ %input#brush_change{:type => "hidden"}
+ .graphs.row
+ #contributors-master
+ #contributors.clearfix
+ %ol.contributors-list.clearfix
diff --git a/app/views/projects/issues/_discussion.html.haml b/app/views/projects/issues/_discussion.html.haml
index b151393abab..c2f4457b60b 100644
--- a/app/views/projects/issues/_discussion.html.haml
+++ b/app/views/projects/issues/_discussion.html.haml
@@ -1,7 +1,7 @@
- content_for :note_actions do
- if can?(current_user, :update_issue, @issue)
- = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-grouped btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
- = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-grouped btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
+ = link_to 'Reopen issue', issue_path(@issue, issue: {state_event: :reopen}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Reopen issue", alternative_text: "Comment & reopen issue"}, class: "btn btn-nr btn-reopen btn-comment js-note-target-reopen #{issue_button_visibility(@issue, false)}", title: 'Reopen issue'
+ = link_to 'Close issue', issue_path(@issue, issue: {state_event: :close}, status_only: true, format: 'json'), data: {no_turbolink: true, original_text: "Close issue", alternative_text: "Comment & close issue"}, class: "btn btn-nr btn-close btn-comment js-note-target-close #{issue_button_visibility(@issue, true)}", title: 'Close issue'
#notes
= render 'projects/notes/notes_with_form'
diff --git a/app/views/projects/merge_requests/_discussion.html.haml b/app/views/projects/merge_requests/_discussion.html.haml
index 393998f15b9..53dd300c35c 100644
--- a/app/views/projects/merge_requests/_discussion.html.haml
+++ b/app/views/projects/merge_requests/_discussion.html.haml
@@ -1,8 +1,8 @@
- content_for :note_actions do
- if can?(current_user, :update_merge_request, @merge_request)
- if @merge_request.open?
- = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-grouped btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
+ = link_to 'Close merge request', merge_request_path(@merge_request, merge_request: {state_event: :close }), method: :put, class: "btn btn-nr btn-comment btn-close close-mr-link js-note-target-close", title: "Close merge request", data: {original_text: "Close merge request", alternative_text: "Comment & close merge request"}
- if @merge_request.closed?
- = link_to 'Reopen merge request', merge_request_path(@merge_request, merge_request: {state_event: :reopen }), method: :put, class: "btn btn-nr btn-comment btn-grouped 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"}
+ = 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"}
#notes= render "projects/notes/notes_with_form"
diff --git a/app/views/projects/network/show.html.haml b/app/views/projects/network/show.html.haml
index e4ab064eda8..593af319a47 100644
--- a/app/views/projects/network/show.html.haml
+++ b/app/views/projects/network/show.html.haml
@@ -15,5 +15,5 @@
= check_box_tag :filter_ref, 1, @options[:filter_ref]
%span Begin with the selected commit
- .network-graph{ data: { url: '#{escape_javascript(@url)}', commit_url: '#{escape_javascript(@commit_url)}', ref: '#{escape_javascript(@ref)}', commit_id: '#{escape_javascript(@commit.id)}' } }
+ .network-graph{ data: { url: @url, commit_url: @commit_url, ref: @ref, commit_id: @commit.id } }
= spinner nil, true
diff --git a/app/views/projects/notes/_edit_form.html.haml b/app/views/projects/notes/_edit_form.html.haml
index c87a3fadf72..8620f492282 100644
--- a/app/views/projects/notes/_edit_form.html.haml
+++ b/app/views/projects/notes/_edit_form.html.haml
@@ -6,6 +6,6 @@
= render 'projects/notes/hints'
.note-form-actions.clearfix
- = f.submit 'Save Comment', class: 'btn btn-nr btn-save btn-grouped js-comment-button'
+ = f.submit 'Save Comment', class: 'btn btn-nr btn-save js-comment-button'
%button.btn.btn-nr.btn-cancel.note-edit-cancel{ type: 'button' }
Cancel
diff --git a/app/views/projects/notes/_form.html.haml b/app/views/projects/notes/_form.html.haml
index 67ed38a7b22..03b3f6935d1 100644
--- a/app/views/projects/notes/_form.html.haml
+++ b/app/views/projects/notes/_form.html.haml
@@ -14,7 +14,7 @@
.error-alert
.note-form-actions.clearfix
- = f.submit 'Comment', class: "btn btn-nr btn-create comment-btn btn-grouped js-comment-button"
+ = f.submit 'Comment', class: "btn btn-nr btn-create append-right-10 comment-btn js-comment-button"
= yield(:note_actions)
%a.btn.btn-cancel.js-note-discard{role: "button", data: {cancel_text: "Cancel"}}
Discard draft
diff --git a/app/views/projects/notes/_hints.html.haml b/app/views/projects/notes/_hints.html.haml
index 0b002043408..7d1cbc62e86 100644
--- a/app/views/projects/notes/_hints.html.haml
+++ b/app/views/projects/notes/_hints.html.haml
@@ -5,4 +5,4 @@
is supported
%button.toolbar-button.markdown-selector{ type: 'button', tabindex: '-1' }
= icon('file-image-o', class: 'toolbar-button-icon')
- Attach a file
+ Attach a file \ No newline at end of file
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
index bcdbff08011..c04d291412c 100644
--- a/app/views/projects/notes/_note.html.haml
+++ b/app/views/projects/notes/_note.html.haml
@@ -18,9 +18,9 @@
= time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
.note-actions
- access = note.project.team.human_max_access(note.author.id)
- - if access
+ - if access and not note.system
%span.note-role.hidden-xs= access
- - if current_user
+ - if current_user and not note.system
= link_to '#', title: 'Award Emoji', class: 'note-action-button note-emoji-button js-add-award js-note-emoji', data: { position: 'right' } do
= icon('spinner spin')
= icon('smile-o')
diff --git a/app/views/projects/project_members/_group_members.html.haml b/app/views/projects/project_members/_group_members.html.haml
index cb6136c215a..e783d8c72c5 100644
--- a/app/views/projects/project_members/_group_members.html.haml
+++ b/app/views/projects/project_members/_group_members.html.haml
@@ -2,8 +2,7 @@
.panel-heading
%strong #{@group.name}
group members
- %small
- (#{members.count})
+ %span.badge= members.size
- if can?(current_user, :admin_group_member, @group)
.controls
= link_to 'Manage group members',
diff --git a/app/views/projects/project_members/_shared_group_members.html.haml b/app/views/projects/project_members/_shared_group_members.html.haml
index 952844acefc..840b57c2e63 100644
--- a/app/views/projects/project_members/_shared_group_members.html.haml
+++ b/app/views/projects/project_members/_shared_group_members.html.haml
@@ -1,6 +1,7 @@
- @project_group_links.each do |group_links|
- shared_group = group_links.group
- - shared_group_users_count = group_links.group.group_members.count
+ - shared_group_members = shared_group.members.non_request
+ - shared_group_users_count = shared_group_members.size
.panel.panel-default
.panel-heading
Shared with
@@ -15,7 +16,7 @@
Edit group members
%ul.content-list
= render partial: 'shared/members/member',
- collection: shared_group.group_members.order(access_level: :desc).limit(20),
+ collection: shared_group_members.order(access_level: :desc).limit(20),
as: :member,
locals: { show_controls: false, show_roles: false }
- if shared_group_users_count > 20
diff --git a/app/views/projects/project_members/_team.html.haml b/app/views/projects/project_members/_team.html.haml
index 03207614258..b0bfdd235f7 100644
--- a/app/views/projects/project_members/_team.html.haml
+++ b/app/views/projects/project_members/_team.html.haml
@@ -2,8 +2,7 @@
.panel-heading
%strong #{@project.name}
project members
- %small
- (#{members.count})
+ %span.badge= members.size
.controls
= form_tag namespace_project_project_members_path(@project.namespace, @project), method: :get, class: 'form-inline member-search-form' do
.form-group
diff --git a/app/views/projects/project_members/index.html.haml b/app/views/projects/project_members/index.html.haml
index 357ccccaf1d..a2026c41d01 100644
--- a/app/views/projects/project_members/index.html.haml
+++ b/app/views/projects/project_members/index.html.haml
@@ -18,7 +18,7 @@
= render 'team', members: @project_members.non_request
- if @group
- = render "group_members", members: @group_members
+ = render "group_members", members: @group_members.non_request
- if @project_group_links.any? && @project.allowed_to_share_with_group?
= render "shared_group_members"
diff --git a/app/views/projects/runners/_form.html.haml b/app/views/projects/runners/_form.html.haml
index d62f5c8f131..c45a9d4f81f 100644
--- a/app/views/projects/runners/_form.html.haml
+++ b/app/views/projects/runners/_form.html.haml
@@ -13,6 +13,12 @@
= f.check_box :run_untagged
%span.light Indicates whether this runner can pick jobs without tags
.form-group
+ = label :locked, 'Lock to current projects', class: 'control-label'
+ .col-sm-10
+ .checkbox
+ = f.check_box :locked
+ %span.light When a runner is locked, it cannot be assigned to other projects
+ .form-group
= label_tag :token, class: 'control-label' do
Token
.col-sm-10
diff --git a/app/views/projects/runners/_runner.html.haml b/app/views/projects/runners/_runner.html.haml
index 96e2aac451f..85225857758 100644
--- a/app/views/projects/runners/_runner.html.haml
+++ b/app/views/projects/runners/_runner.html.haml
@@ -2,8 +2,10 @@
%h4
= runner_status_icon(runner)
%span.monospace
- - if @runners.include?(runner)
+ - if @project_runners.include?(runner)
= link_to runner.short_sha, runner_path(runner)
+ - if runner.locked?
+ = icon('lock', class: 'has-tooltip', title: 'Locked to current projects')
%small
= link_to edit_namespace_project_runner_path(@project.namespace, @project, runner) do
%i.fa.fa-edit.btn
@@ -11,7 +13,7 @@
= runner.short_sha
.pull-right
- - if @runners.include?(runner)
+ - 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'
- else
diff --git a/app/views/projects/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 8ae9f0d95f7..d469dda5b81 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -17,13 +17,13 @@
Start runner!
-- if @runners.any?
+- if @project_runners.any?
%h4.underlined-title Runners activated for this project
%ul.bordered-list.activated-specific-runners
- = render partial: 'runner', collection: @runners, as: :runner
+ = render partial: 'runner', collection: @project_runners, as: :runner
-- if @specific_runners.any?
+- if @assignable_runners.any?
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
- = render partial: 'runner', collection: @specific_runners, as: :runner
- = paginate @specific_runners
+ = render partial: 'runner', collection: @assignable_runners, as: :runner
+ = paginate @assignable_runners
diff --git a/app/views/projects/runners/show.html.haml b/app/views/projects/runners/show.html.haml
index f24e1b9144e..61b99f35d74 100644
--- a/app/views/projects/runners/show.html.haml
+++ b/app/views/projects/runners/show.html.haml
@@ -23,6 +23,9 @@
%td Can run untagged jobs
%td= @runner.run_untagged? ? 'Yes' : 'No'
%tr
+ %td Locked to this project
+ %td= @runner.locked? ? 'Yes' : 'No'
+ %tr
%td Tags
%td
- @runner.tag_list.each do |tag|
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index e9ca46a74bf..15f0d85194b 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -57,6 +57,10 @@
%li.missing
= link_to add_special_file_path(@project, file_name: 'CONTRIBUTING.md', commit_message: 'Add contribution guide') do
Add Contribution guide
+ - unless @repository.gitlab_ci_yml
+ %li.missing
+ = link_to add_special_file_path(@project, file_name: '.gitlab-ci.yml') do
+ Set Up CI
- if @repository.commit
.content-block.second-block.white
diff --git a/app/views/projects/tags/_tag.html.haml b/app/views/projects/tags/_tag.html.haml
index 844e1055810..2c11c0e5b21 100644
--- a/app/views/projects/tags/_tag.html.haml
+++ b/app/views/projects/tags/_tag.html.haml
@@ -15,7 +15,7 @@
= render 'projects/tags/download', ref: tag.name, project: @project
- if can?(current_user, :push_code, @project)
- = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes" do
+ = link_to edit_namespace_project_tag_release_path(@project.namespace, @project, tag.name), class: 'btn has-tooltip', title: "Edit release notes", data: { container: "body" } do
= icon("pencil")
- if can?(current_user, :admin_project, @project)
diff --git a/app/views/projects/wikis/_main_links.html.haml b/app/views/projects/wikis/_main_links.html.haml
index 4faa547769b..4ea75dbbf0c 100644
--- a/app/views/projects/wikis/_main_links.html.haml
+++ b/app/views/projects/wikis/_main_links.html.haml
@@ -1,4 +1,7 @@
- if (@page && @page.persisted?)
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
= link_to namespace_project_wiki_history_path(@project.namespace, @project, @page), class: "btn" do
Page History
- if can?(current_user, :create_wiki, @project)
diff --git a/app/views/projects/wikis/_nav.html.haml b/app/views/projects/wikis/_nav.html.haml
index 988fe024e28..f8ea479e0b1 100644
--- a/app/views/projects/wikis/_nav.html.haml
+++ b/app/views/projects/wikis/_nav.html.haml
@@ -1,5 +1,5 @@
-.top-area
- %ul.nav-links
+.nav-links.sub-nav
+ %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)
@@ -10,9 +10,4 @@
= link_to namespace_project_wikis_git_access_path(@project.namespace, @project) do
Git Access
- .nav-controls
- - if can?(current_user, :create_wiki, @project)
- = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
- New Page
-
-= render 'projects/wikis/new'
+ = render 'projects/wikis/new'
diff --git a/app/views/projects/wikis/_new.html.haml b/app/views/projects/wikis/_new.html.haml
index 919daf0a7b2..4f8abcdc8e1 100644
--- a/app/views/projects/wikis/_new.html.haml
+++ b/app/views/projects/wikis/_new.html.haml
@@ -1,14 +1,17 @@
-%div#modal-new-wiki.modal
- .modal-dialog
- .modal-content
- .modal-header
- %a.close{href: "#", "data-dismiss" => "modal"} ×
- %h3.page-title New Wiki Page
- .modal-body
- %form.new-wiki-page
- .form-group
- = label_tag :new_wiki_path do
- %span Page slug
- = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
- .form-actions
- = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
+- @no_container = true
+
+%div{ class: (container_class) }
+ %div#modal-new-wiki.modal
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %a.close{href: "#", "data-dismiss" => "modal"} ×
+ %h3.page-title New Wiki Page
+ .modal-body
+ %form.new-wiki-page
+ .form-group
+ = label_tag :new_wiki_path do
+ %span Page slug
+ = text_field_tag :new_wiki_path, nil, placeholder: 'how-to-setup', class: 'form-control', required: true, :'data-wikis-path' => namespace_project_wikis_path(@project.namespace, @project), autofocus: true
+ .form-actions
+ = button_tag 'Create Page', class: 'build-new-wiki btn btn-create'
diff --git a/app/views/projects/wikis/edit.html.haml b/app/views/projects/wikis/edit.html.haml
index cbd69ee1a73..bf5d09d50c2 100644
--- a/app/views/projects/wikis/edit.html.haml
+++ b/app/views/projects/wikis/edit.html.haml
@@ -1,19 +1,24 @@
+- @no_container = true
- page_title "Edit", @page.title.capitalize, "Wiki"
= render 'nav'
-.top-area
- .nav-text.wiki-page
- %strong
- - if @page.persisted?
- = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- - else
- = @page.title.capitalize
- %span.light
- &middot;
- Edit Page
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ %strong
+ - if @page.persisted?
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ - else
+ = @page.title.capitalize
+ %span.light
+ &middot;
+ Edit Page
- .nav-controls
- = render 'main_links'
+ .nav-controls
+ - if can?(current_user, :create_wiki, @project)
+ = link_to '#modal-new-wiki', class: "add-new-wiki btn btn-new", "data-toggle" => "modal" do
+ New Page
+ = render 'main_links'
-= render 'form'
+ = render 'form'
diff --git a/app/views/projects/wikis/git_access.html.haml b/app/views/projects/wikis/git_access.html.haml
index ccceab6155e..6caf7230f35 100644
--- a/app/views/projects/wikis/git_access.html.haml
+++ b/app/views/projects/wikis/git_access.html.haml
@@ -1,32 +1,34 @@
+- @no_container = true
- page_title "Git Access", "Wiki"
= render 'nav'
-.row-content-block
- %span.oneline
- Git access for
- %strong= @project_wiki.path_with_namespace
+%div{ class: (container_class) }
+ .sub-header-block
+ %span.oneline
+ Git access for
+ %strong= @project_wiki.path_with_namespace
- .pull-right
- = render "shared/clone_panel", project: @project_wiki
+ .pull-right
+ = render "shared/clone_panel", project: @project_wiki
-.git-empty.prepend-top-default
- %fieldset
- %legend Install Gollum:
- %pre.dark
- :preserve
- gem install gollum
+ .prepend-top-default
+ %fieldset
+ %legend Install Gollum:
+ %pre.dark
+ :preserve
+ gem install gollum
- %legend Clone Your Wiki:
- %pre.dark
- :preserve
- git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
- cd #{h @project_wiki.path}
+ %legend Clone Your Wiki:
+ %pre.dark
+ :preserve
+ git clone #{ content_tag(:span, default_url_to_repo(@project_wiki), class: 'clone')}
+ cd #{h @project_wiki.path}
- %legend Start Gollum And Edit Locally:
- %pre.dark
- :preserve
- gollum
- == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
- >> Thin web server (v1.5.0 codename Knife)
- >> Maximum connections set to 1024
- >> Listening on 0.0.0.0:4567, CTRL+C to stop
+ %legend Start Gollum And Edit Locally:
+ %pre.dark
+ :preserve
+ gollum
+ == Sinatra/1.3.5 has taken the stage on 4567 for development with backup from Thin
+ >> Thin web server (v1.5.0 codename Knife)
+ >> Maximum connections set to 1024
+ >> Listening on 0.0.0.0:4567, CTRL+C to stop
diff --git a/app/views/projects/wikis/history.html.haml b/app/views/projects/wikis/history.html.haml
index 45460ed9f41..630ee35b70b 100644
--- a/app/views/projects/wikis/history.html.haml
+++ b/app/views/projects/wikis/history.html.haml
@@ -1,37 +1,37 @@
- page_title "History", @page.title.capitalize, "Wiki"
= render 'nav'
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ %strong
+ = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
+ %span.light
+ &middot;
+ History
-.top-area
- .nav-text
- %strong
- = link_to @page.title.capitalize, namespace_project_wiki_path(@project.namespace, @project, @page)
- %span.light
- &middot;
- History
-
-.table-holder
- %table.table
- %thead
- %tr
- %th Page version
- %th Author
- %th Commit Message
- %th Last updated
- %th Format
- %tbody
- - @page.versions.each_with_index do |version, index|
- - commit = version
+ .table-holder
+ %table.table
+ %thead
%tr
- %td
- = link_to project_wiki_path_with_version(@project, @page,
- commit.id, index == 0) do
- = truncate_sha(commit.id)
- %td
- = commit.author.name
- %td
- = commit.message
- %td
- #{time_ago_with_tooltip(version.authored_date)}
- %td
- %strong
- = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
+ %th Page version
+ %th Author
+ %th Commit Message
+ %th Last updated
+ %th Format
+ %tbody
+ - @page.versions.each_with_index do |version, index|
+ - commit = version
+ %tr
+ %td
+ = link_to project_wiki_path_with_version(@project, @page,
+ commit.id, index == 0) do
+ = truncate_sha(commit.id)
+ %td
+ = commit.author.name
+ %td
+ = commit.message
+ %td
+ #{time_ago_with_tooltip(version.authored_date)}
+ %td
+ %strong
+ = @page.page.wiki.page(@page.page.name, commit.id).try(:format)
diff --git a/app/views/projects/wikis/pages.html.haml b/app/views/projects/wikis/pages.html.haml
index 2f6162fa3c5..81d9f391c1c 100644
--- a/app/views/projects/wikis/pages.html.haml
+++ b/app/views/projects/wikis/pages.html.haml
@@ -1,12 +1,14 @@
+- @no_container = true
- page_title "Pages", "Wiki"
= render 'nav'
-%ul.content-list
- - @wiki_pages.each do |wiki_page|
- %li
- = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
- %small (#{wiki_page.format})
- .pull-right
- %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
-= paginate @wiki_pages, theme: 'gitlab'
+%div{ class: (container_class) }
+ %ul.content-list
+ - @wiki_pages.each do |wiki_page|
+ %li
+ = link_to wiki_page.title, namespace_project_wiki_path(@project.namespace, @project, wiki_page)
+ %small (#{wiki_page.format})
+ .pull-right
+ %small Last edited #{time_ago_with_tooltip(wiki_page.commit.authored_date)}
+ = paginate @wiki_pages, theme: 'gitlab'
diff --git a/app/views/projects/wikis/show.html.haml b/app/views/projects/wikis/show.html.haml
index 9166c0edb3b..76f9b1ecd76 100644
--- a/app/views/projects/wikis/show.html.haml
+++ b/app/views/projects/wikis/show.html.haml
@@ -1,24 +1,26 @@
+- @no_container = true
- page_title @page.title.capitalize, "Wiki"
= render 'nav'
-.top-area
- .nav-text
- %strong= @page.title.capitalize
+%div{ class: (container_class) }
+ .top-area
+ .nav-text
+ %strong= @page.title.capitalize
- %span.wiki-last-edit-by
- &middot;
- last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
+ %span.wiki-last-edit-by
+ &middot;
+ last edited by #{@page.commit.author.name} #{time_ago_with_tooltip(@page.commit.authored_date)}
- .nav-controls
- = render 'main_links'
+ .nav-controls
+ = render 'main_links'
-- if @page.historical?
- .warning_message
- This is an old version of this page.
- You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
+ - if @page.historical?
+ .warning_message
+ This is an old version of this page.
+ You can view the #{link_to "most recent version", namespace_project_wiki_path(@project.namespace, @project, @page)} or browse the #{link_to "history", namespace_project_wiki_history_path(@project.namespace, @project, @page)}.
-.wiki-holder.prepend-top-default.append-bottom-default
- .wiki
- = preserve do
- = render_wiki_content(@page)
+ .wiki-holder.prepend-top-default.append-bottom-default
+ .wiki
+ = preserve do
+ = render_wiki_content(@page)
diff --git a/app/views/shared/_event_filter.html.haml b/app/views/shared/_event_filter.html.haml
index 30055002213..aa18e6f236f 100644
--- a/app/views/shared/_event_filter.html.haml
+++ b/app/views/shared/_event_filter.html.haml
@@ -1,7 +1,9 @@
%ul.nav-links.event-filter.scrolling-tabs
- .fade-left
+ %li.fade-left
+ = icon('arrow-left')
= event_filter_link EventFilter.push, 'Push events'
= event_filter_link EventFilter.merged, 'Merge events'
= event_filter_link EventFilter.comments, 'Comments'
= event_filter_link EventFilter.team, 'Team'
- .fade-right
+ %li.fade-right
+ = icon('arrow-right')
diff --git a/app/views/shared/_ref_switcher.html.haml b/app/views/shared/_ref_switcher.html.haml
index eb2e1919e19..ea7162d4d63 100644
--- a/app/views/shared/_ref_switcher.html.haml
+++ b/app/views/shared/_ref_switcher.html.haml
@@ -1,7 +1,14 @@
+- dropdown_toggle_text = @ref || @project.default_branch
= form_tag switch_namespace_project_refs_path(@project.namespace, @project), method: :get, class: "project-refs-form" do
- = select_tag "ref", grouped_options_refs, class: "project-refs-select select2 select2-sm"
= hidden_field_tag :destination, destination
- if defined?(path)
= hidden_field_tag :path, path
- @options && @options.each do |key, value|
= hidden_field_tag key, value, id: nil
+ .dropdown
+ = dropdown_toggle dropdown_toggle_text, { toggle: "dropdown", selected: dropdown_toggle_text, ref: @ref, refs_url: refs_namespace_project_path(@project.namespace, @project) }, { toggle_class: "js-project-refs-dropdown" }
+ .dropdown-menu.dropdown-menu-selectable{ class: ("dropdown-menu-align-right" if local_assigns[:align_right]) }
+ = dropdown_title "Switch branch/tag"
+ = dropdown_filter "Search branches and tags"
+ = dropdown_content
+ = dropdown_loading
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 210c9b9aab5..adfab1af53e 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -1,4 +1,4 @@
-- todo = has_todo(issuable)
+- todo = issuable_todo(issuable)
%aside.right-sidebar{ class: sidebar_gutter_collapsed_class }
.issuable-sidebar
- can_edit_issuable = can?(current_user, :"admin_#{issuable.to_ability_name}", @project)
@@ -9,12 +9,12 @@
%a.gutter-toggle.pull-right.js-sidebar-toggle{ role: "button", href: "#", aria: { label: "Toggle sidebar" } }
= sidebar_gutter_toggle_icon
- if current_user
- %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", id: (todo.id unless todo.nil?), issuable: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project) } }
+ %button.btn.btn-default.issuable-header-btn.pull-right.js-issuable-todo{ type: "button", aria: { label: (todo.nil? ? "Add Todo" : "Mark Done") }, data: { todo_text: "Add Todo", mark_text: "Mark Done", issuable_id: issuable.id, issuable_type: issuable.class.name.underscore, url: namespace_project_todos_path(@project.namespace, @project), delete_path: (dashboard_todo_path(todo) if todo) } }
%span.js-issuable-todo-text
- - if todo.nil?
- Add Todo
- - else
+ - if todo
Mark Done
+ - else
+ Add Todo
= icon('spin spinner', class: 'hidden js-issuable-todo-loading')
= form_for [@project.namespace.becomes(Namespace), @project, issuable], remote: true, format: :json, html: {class: 'issuable-context-form inline-update js-issuable-update'} do |f|
diff --git a/app/views/shared/members/_access_request_buttons.html.haml b/app/views/shared/members/_access_request_buttons.html.haml
index ed0a6ebcf84..480e8ba6c85 100644
--- a/app/views/shared/members/_access_request_buttons.html.haml
+++ b/app/views/shared/members/_access_request_buttons.html.haml
@@ -1,12 +1,14 @@
- member = source.members.find_by(user_id: current_user.id)
+- group_member = source.group.members.find_by(user_id: current_user.id) if source.respond_to?(:group) && source.group
-- if member
- - if member.request?
- = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
- method: :delete,
- data: { confirm: remove_member_message(member) },
+- unless group_member
+ - if member
+ - if member.request?
+ = link_to 'Withdraw Access Request', polymorphic_path([:leave, source, :members]),
+ method: :delete,
+ data: { confirm: remove_member_message(member) },
+ class: 'btn access-request-button hidden-xs'
+ - else
+ = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
+ method: :post,
class: 'btn access-request-button hidden-xs'
-- else
- = link_to 'Request Access', polymorphic_path([:request_access, source, :members]),
- method: :post,
- class: 'btn access-request-button hidden-xs'
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 0191814849a..a884e78e6e7 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -1,5 +1,4 @@
-- default_show_roles = can?(current_user, action_member_permission(:update, member), member) || can?(current_user, action_member_permission(:destroy, member), member)
-- show_roles = local_assigns.fetch(:show_roles, default_show_roles)
+- show_roles = local_assigns.fetch(:show_roles, default_show_roles(member))
- show_controls = local_assigns.fetch(:show_controls, true)
- user = member.user
diff --git a/app/views/shared/members/_requests.html.haml b/app/views/shared/members/_requests.html.haml
index b5963876034..e4bd2bdc265 100644
--- a/app/views/shared/members/_requests.html.haml
+++ b/app/views/shared/members/_requests.html.haml
@@ -3,6 +3,6 @@
.panel-heading
%strong= membership_source.name
access requests
- %small= "(#{members.size})"
+ %span.badge= members.size
%ul.content-list
= render partial: 'shared/members/member', collection: members, as: :member
diff --git a/app/views/shared/milestones/_summary.html.haml b/app/views/shared/milestones/_summary.html.haml
index 385c6596606..975c74f4ea6 100644
--- a/app/views/shared/milestones/_summary.html.haml
+++ b/app/views/shared/milestones/_summary.html.haml
@@ -10,6 +10,13 @@
open and
%strong= milestone.issues_visible_to_user(current_user).closed.size
closed
+ %strong= milestone.merge_requests.size
+ merge requests:
+ %span.milestone-stat
+ %strong= milestone.merge_requests.opened.size
+ open and
+ %strong= milestone.merge_requests.merged.size
+ merged
%span.milestone-stat
%strong== #{milestone.percent_complete(current_user)}%
complete
diff --git a/app/views/shared/notifications/_button.html.haml b/app/views/shared/notifications/_button.html.haml
new file mode 100644
index 00000000000..ff1cf966a9b
--- /dev/null
+++ b/app/views/shared/notifications/_button.html.haml
@@ -0,0 +1,25 @@
+- left_align = local_assigns[:left_align]
+- if notification_setting
+ .dropdown.notification-dropdown.pull-right
+ = form_for notification_setting, remote: true, html: { class: "inline notification-form" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ = f.hidden_field :level, class: "notification_setting_level"
+ .js-notification-toggle-btns
+ %div{ class: ("btn-group" if notification_setting.custom?) }
+ - if notification_setting.custom?
+ %button.dropdown-new.btn.btn-default.notifications-btn#notifications-button{ type: "button", data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting) } }
+ = 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
+ .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) } }
+ = icon("bell", class: "js-notification-loading")
+ = notification_title(notification_setting.level)
+ = icon("caret-down")
+
+ = render "shared/notifications/notification_dropdown", notification_setting: notification_setting, left_align: left_align
+
+ = content_for :scripts_body do
+ = render "shared/notifications/custom_notifications", notification_setting: notification_setting
diff --git a/app/views/shared/notifications/_custom_notifications.html.haml b/app/views/shared/notifications/_custom_notifications.html.haml
new file mode 100644
index 00000000000..b704981e3db
--- /dev/null
+++ b/app/views/shared/notifications/_custom_notifications.html.haml
@@ -0,0 +1,31 @@
+.modal.fade{ tabindex: "-1", role: "dialog", id: notifications_menu_identifier("modal", notification_setting), aria: { labelledby: "custom-notifications-title" } }
+ .modal-dialog
+ .modal-content
+ .modal-header
+ %button.close{ type: "button", data: { dismiss: "modal" }, aria: { label: "close" } }
+ %span{ aria: { hidden: "true" } } ×
+ %h4#custom-notifications-title.modal-title
+ Custom notification events
+
+ .modal-body
+ .container-fluid
+ = form_for notification_setting, html: { class: "custom-notifications-form" } do |f|
+ = hidden_setting_source_input(notification_setting)
+ .row
+ .col-lg-4
+ %h4.prepend-top-0
+ Notification events
+ %p
+ Custom notification levels are the same as participating levels. With custom notification levels you will also receive notifications for select events. To find out more, check out
+ = succeed "." do
+ %a{ href: "http://docs.gitlab.com/ce/workflow/notifications.html", target: "_blank"} notification emails
+ .col-lg-8
+ - NotificationSetting::EMAIL_EVENTS.each_with_index do |event, index|
+ - field_id = "#{notifications_menu_identifier("modal", notification_setting)}_notification_setting[#{event}]"
+ .form-group
+ .checkbox{ class: ("prepend-top-0" if index == 0) }
+ %label{ for: field_id }
+ = check_box("notification_setting", event, id: field_id, class: "js-custom-notification-event", checked: notification_setting.events[event])
+ %strong
+ = event.to_s.humanize
+ = icon("spinner spin", class: "custom-notification-event-loading")
diff --git a/app/views/shared/notifications/_notification_dropdown.html.haml b/app/views/shared/notifications/_notification_dropdown.html.haml
new file mode 100644
index 00000000000..d3258ee64cb
--- /dev/null
+++ b/app/views/shared/notifications/_notification_dropdown.html.haml
@@ -0,0 +1,13 @@
+- left_align = local_assigns[:left_align]
+%ul.dropdown-menu.dropdown-menu-no-wrap.dropdown-menu-selectable.dropdown-menu-large{ role: "menu", class: [notifications_menu_identifier("dropdown", notification_setting), ("dropdown-menu-align-right" unless left_align)] }
+ - NotificationSetting.levels.each_key do |level|
+ - next if level == "custom"
+ - next if level == "global" && notification_setting.source.nil?
+
+ = notification_list_item(level, notification_setting)
+
+ %li.divider
+ %li
+ %a.update-notification{ href: "#", role: "button", class: ("is-active" if notification_setting.custom?), data: { toggle: "modal", target: "#" + notifications_menu_identifier("modal", notification_setting), notification_level: "custom", notification_title: "Custom" } }
+ %strong.dropdown-menu-inner-title Custom
+ %span.dropdown-menu-inner-content= notification_description("custom")
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 2e08bb2ac08..3a9dd37dc7d 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -16,6 +16,12 @@
= render "shared/projects/project", project: project, skip_namespace: skip_namespace,
avatar: avatar, stars: stars, css_class: css_class, ci: ci, use_creator_avatar: use_creator_avatar,
forks: forks, show_last_commit_as_description: show_last_commit_as_description
+
+ - if @private_forks_count && @private_forks_count > 0
+ %li.project-row.private-forks-notice
+ = icon('lock fw', base: 'circle', class: 'fa-lg private-fork-icon')
+ %strong= pluralize(@private_forks_count, 'private fork')
+ %span you have no access to.
= paginate(projects, remote: remote, theme: "gitlab") if projects.respond_to? :total_pages
- else
.nothing-here-block No projects found
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 09ffc319065..c6dc1e4ab38 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -215,7 +215,7 @@ Settings.gitlab.default_projects_features['container_registry'] = true if Settin
Settings.gitlab.default_projects_features['visibility_level'] = Settings.send(:verify_constant, Gitlab::VisibilityLevel, Settings.gitlab.default_projects_features['visibility_level'], Gitlab::VisibilityLevel::PRIVATE)
Settings.gitlab['repository_downloads_path'] = File.join(Settings.shared['path'], 'cache/archive') if Settings.gitlab['repository_downloads_path'].nil?
Settings.gitlab['restricted_signup_domains'] ||= []
-Settings.gitlab['import_sources'] ||= ['github','bitbucket','gitlab','gitorious','google_code','fogbugz','git']
+Settings.gitlab['import_sources'] ||= %w[github bitbucket gitlab gitorious google_code fogbugz git gitlab_project]
Settings.gitlab['trusted_proxies'] ||= []
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index f1eec674888..7a2b9a7f6c1 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -23,6 +23,10 @@ Sidekiq.configure_server do |config|
config['pool'] = Sidekiq.options[:concurrency] + 2
ActiveRecord::Base.establish_connection(config)
Rails.logger.debug("Connection Pool size for Sidekiq Server is now: #{ActiveRecord::Base.connection.pool.instance_variable_get('@size')}")
+
+ # Avoid autoload issue such as 'Mail::Parsers::AddressStruct'
+ # https://github.com/mikel/mail/issues/912#issuecomment-214850355
+ Mail.eager_autoload!
end
Sidekiq.configure_client do |config|
diff --git a/config/routes.rb b/config/routes.rb
index 87da5e7178f..e45293cdf7f 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -123,10 +123,18 @@ 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
@@ -287,7 +295,7 @@ Rails.application.routes.draw do
post :repository_check
end
- resources :runner_projects
+ resources :runner_projects, only: [:create, :destroy]
end
end
@@ -432,7 +440,6 @@ Rails.application.routes.draw do
resource :avatar, only: [:destroy]
resources :milestones, constraints: { id: /[^\/]+/ }, only: [:index, :show, :update, :new, :create]
- resource :notification_setting, only: [:update]
end
end
@@ -472,6 +479,7 @@ Rails.application.routes.draw do
get :download_export
get :autocomplete_sources
get :activity
+ get :refs
end
scope module: :projects do
@@ -668,7 +676,6 @@ Rails.application.routes.draw do
resources :forks, only: [:index, :new, :create]
resource :import, only: [:new, :create, :show]
- resource :notification_setting, only: [:update]
resources :refs, only: [] do
collection do
@@ -812,7 +819,7 @@ Rails.application.routes.draw do
end
end
- resources :todos, only: [:create, :update], constraints: { id: /\d+/ }
+ resources :todos, only: [:create]
resources :uploads, only: [:create] do
collection do
diff --git a/db/migrate/20160509091049_add_locked_to_ci_runner.rb b/db/migrate/20160509091049_add_locked_to_ci_runner.rb
new file mode 100644
index 00000000000..3fbaef3b7f0
--- /dev/null
+++ b/db/migrate/20160509091049_add_locked_to_ci_runner.rb
@@ -0,0 +1,13 @@
+class AddLockedToCiRunner < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_runners, :locked, :boolean,
+ default: false, allow_null: false)
+ end
+
+ def down
+ remove_column(:ci_runners, :locked)
+ end
+end
diff --git a/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
new file mode 100644
index 00000000000..bd0463886bc
--- /dev/null
+++ b/db/migrate/20160615191922_set_missing_stage_on_ci_builds.rb
@@ -0,0 +1,9 @@
+class SetMissingStageOnCiBuilds < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def up
+ update_column_in_batches(:ci_builds, :stage, :test) do |table, query|
+ query.where(table[:stage].eq(nil))
+ end
+ end
+end
diff --git a/db/migrate/20160616102642_remove_duplicated_keys.rb b/db/migrate/20160616102642_remove_duplicated_keys.rb
new file mode 100644
index 00000000000..00a45d7fe73
--- /dev/null
+++ b/db/migrate/20160616102642_remove_duplicated_keys.rb
@@ -0,0 +1,19 @@
+# rubocop:disable all
+class RemoveDuplicatedKeys < ActiveRecord::Migration
+ def up
+ select_all("SELECT fingerprint FROM #{quote_table_name(:keys)} GROUP BY fingerprint HAVING COUNT(*) > 1").each do |row|
+ fingerprint = connection.quote(row['fingerprint'])
+ execute(%Q{
+ DELETE FROM keys
+ WHERE fingerprint = #{fingerprint}
+ AND id != (
+ SELECT id FROM (
+ SELECT max(id) AS id
+ FROM keys
+ WHERE fingerprint = #{fingerprint}
+ ) max_ids
+ )
+ })
+ end
+ end
+end
diff --git a/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
new file mode 100644
index 00000000000..4bb4204cebd
--- /dev/null
+++ b/db/migrate/20160616103005_remove_keys_fingerprint_index_if_exists.rb
@@ -0,0 +1,21 @@
+class RemoveKeysFingerprintIndexIfExists < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ # https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/250
+ # That MR was added on gitlab-ee so we need to check if the index
+ # already exists because we want to do is create an unique index instead.
+
+ def up
+ if index_exists?(:keys, :fingerprint)
+ remove_index :keys, :fingerprint
+ end
+ end
+
+ def down
+ unless index_exists?(:keys, :fingerprint)
+ add_concurrent_index :keys, :fingerprint
+ end
+ end
+end
diff --git a/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
new file mode 100644
index 00000000000..e35af38aac3
--- /dev/null
+++ b/db/migrate/20160616103948_add_unique_index_to_keys_fingerprint.rb
@@ -0,0 +1,13 @@
+class AddUniqueIndexToKeysFingerprint < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :keys, :fingerprint, unique: true
+ end
+
+ def down
+ remove_index :keys, :fingerprint
+ end
+end
diff --git a/db/migrate/20160617301627_add_events_to_notification_settings.rb b/db/migrate/20160617301627_add_events_to_notification_settings.rb
new file mode 100644
index 00000000000..609596f45e4
--- /dev/null
+++ b/db/migrate/20160617301627_add_events_to_notification_settings.rb
@@ -0,0 +1,7 @@
+class AddEventsToNotificationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ def change
+ add_column :notification_settings, :events, :text
+ end
+end
diff --git a/db/migrate/20160620115026_add_index_on_runners_locked.rb b/db/migrate/20160620115026_add_index_on_runners_locked.rb
new file mode 100644
index 00000000000..dfa5110dea4
--- /dev/null
+++ b/db/migrate/20160620115026_add_index_on_runners_locked.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 AddIndexOnRunnersLocked < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ def change
+ add_concurrent_index :ci_runners, :locked
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 5a27e9d5cdc..7a8377f687c 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: 20160616084004) do
+ActiveRecord::Schema.define(version: 20160620115026) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -287,9 +287,11 @@ ActiveRecord::Schema.define(version: 20160616084004) do
t.string "platform"
t.string "architecture"
t.boolean "run_untagged", default: true, null: false
+ t.boolean "locked", default: false, null: false
end
add_index "ci_runners", ["description"], name: "index_ci_runners_on_description_trigram", using: :gin, opclasses: {"description"=>"gin_trgm_ops"}
+ add_index "ci_runners", ["locked"], name: "index_ci_runners_on_locked", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token", using: :btree
add_index "ci_runners", ["token"], name: "index_ci_runners_on_token_trigram", using: :gin, opclasses: {"token"=>"gin_trgm_ops"}
@@ -507,6 +509,7 @@ ActiveRecord::Schema.define(version: 20160616084004) do
end
add_index "keys", ["created_at", "id"], name: "index_keys_on_created_at_and_id", using: :btree
+ add_index "keys", ["fingerprint"], name: "index_keys_on_fingerprint", unique: true, using: :btree
add_index "keys", ["user_id"], name: "index_keys_on_user_id", using: :btree
create_table "label_links", force: :cascade do |t|
@@ -707,6 +710,7 @@ ActiveRecord::Schema.define(version: 20160616084004) do
t.integer "level", default: 0, null: false
t.datetime "created_at", null: false
t.datetime "updated_at", null: false
+ t.text "events"
end
add_index "notification_settings", ["source_id", "source_type"], name: "index_notification_settings_on_source_id_and_source_type", using: :btree
diff --git a/doc/README.md b/doc/README.md
index 5d89d0c9821..f1283cea0ad 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -3,17 +3,18 @@
## User documentation
- [API](api/README.md) Automate GitLab via a simple and powerful API.
-- [CI](ci/README.md) GitLab Continuous Integration (CI) getting started, `.gitlab-ci.yml` options, and examples.
+- [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.
- [Importing to GitLab](workflow/importing/README.md).
+- [Importing and exporting projects between instances](user/project/settings/import_export.md).
- [Markdown](markdown/markdown.md) GitLab's advanced formatting system.
-- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab
+- [Migrating from SVN](workflow/importing/migrating_from_svn.md) Convert a SVN repository to Git and GitLab.
- [Permissions](permissions/permissions.md) Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
- [Profile Settings](profile/README.md)
- [Project Services](project_services/project_services.md) Integrate a project with external services, such as CI and chat.
- [Public access](public_access/public_access.md) Learn how you can allow public and internal access to projects.
-- [Container Registry](container_registry/README.md) Learn how to use GitLab Container Registry.
- [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.
@@ -24,15 +25,15 @@
external authentication with LDAP, SAML, CAS and additional Omniauth providers.
- [Custom git hooks](hooks/custom_hooks.md) Custom git hooks (on the filesystem) for when webhooks aren't enough.
- [Install](install/README.md) Requirements, directory structures and installation from source.
-- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components
+- [Restart GitLab](administration/restart_gitlab.md) Learn how to restart GitLab and its components.
- [Integration](integration/README.md) How to integrate with systems such as JIRA, Redmine, Twitter.
- [Issue closing](customization/issue_closing.md) Customize how to close an issue from commit messages.
- [Libravatar](customization/libravatar.md) Use Libravatar for user avatars.
- [Log system](administration/logs.md) Log system.
- [Environment Variables](administration/environment_variables.md) to configure GitLab.
-- [Operations](operations/README.md) Keeping GitLab up and running
+- [Operations](operations/README.md) Keeping GitLab up and running.
- [Raketasks](raketasks/README.md) Backups, maintenance, automatic webhook setup and the importing of projects.
-- [Repository checks](administration/repository_checks.md) Periodic Git repository checks
+- [Repository checks](administration/repository_checks.md) Periodic Git repository checks.
- [Security](security/README.md) Learn what you can do to further secure your GitLab instance.
- [System hooks](system_hooks/system_hooks.md) Notifications when users, projects and keys are changed.
- [Update](update/README.md) Update guides to upgrade your installation.
@@ -41,11 +42,11 @@
- [Migrate GitLab CI to CE/EE](migrate_ci_to_ce/README.md) Follow this guide to migrate your existing GitLab CI data to GitLab CE/EE.
- [Git LFS configuration](workflow/lfs/lfs_administration.md)
- [Housekeeping](administration/housekeeping.md) Keep your Git repository tidy and fast.
-- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics
-- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint
-- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs
-- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability
-- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab
+- [GitLab Performance Monitoring](monitoring/performance/introduction.md) Configure GitLab and InfluxDB for measuring performance metrics.
+- [Monitoring uptime](monitoring/health_check.md) Check the server status using the health check endpoint.
+- [Sidekiq Troubleshooting](administration/troubleshooting/sidekiq.md) Debug when Sidekiq appears hung and is not processing jobs.
+- [High Availability](administration/high_availability/README.md) Configure multiple servers for scaling or high availability.
+- [Container Registry](administration/container_registry.md) Configure Docker Registry with GitLab.
## Contributor documentation
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index 7870669fa77..d5d43303454 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -22,6 +22,7 @@ You can read more about Docker Registry at https://docs.docker.com/registry/intr
- [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)
@@ -84,6 +85,17 @@ GitLab does not ship with a Registry init file. Hence, [restarting GitLab][resta
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:
+
+```
+auth:
+ token:
+ realm: https://gitlab.example.com/jwt/auth
+ service: container_registry
+ issuer: gitlab-issuer
+ rootcertbundle: /root/certs/certbundle
+```
+
## Container Registry domain configuration
There are two ways you can configure the Registry's external domain.
@@ -306,8 +318,12 @@ the Container Registry by themselves, follow the steps below.
## Container Registry storage path
-To change the storage path where Docker images will be stored, follow the
-steps below.
+>**Note:**
+For configuring storage in the cloud instead of the filesystem, see the
+[storage driver configuration](#container-registry-storage-driver).
+
+If you want to store your images on the filesystem, you can change the storage
+path for the Container Registry, follow the steps below.
This path is accessible to:
@@ -349,6 +365,72 @@ The default location where images are stored in source installations, is
1. Save the file and [restart GitLab][] for the changes to take effect.
+## Container Registry storage driver
+
+You can configure the Container Registry to use a different storage backend by
+configuring a different storage driver. By default the GitLab Container Registry
+is configured to use the filesystem driver, which makes use of [storage path](#container-registry-storage-path)
+configuration.
+
+The different supported drivers are:
+
+| Driver | Description |
+|------------|-------------------------------------|
+| filesystem | Uses a path on the local filesystem |
+| azure | Microsoft Azure Blob Storage |
+| gcs | Google Cloud Storage |
+| s3 | Amazon Simple Storage Service |
+| swift | OpenStack Swift Object Storage |
+| oss | Aliyun OSS |
+
+Read more about the individual driver's config options in the
+[Docker Registry docs][storage-config].
+
+> **Warning** GitLab will not backup Docker images that are not stored on the
+filesystem. Remember to enable backups with your object storage provider if
+desired.
+
+---
+
+**Omnibus GitLab installations**
+
+To configure the storage driver in Omnibus:
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ registry['storage'] = {
+ 's3' => {
+ 'accesskey' => 's3-access-key',
+ 'secretkey' => 's3-secret-key-for-access-key',
+ 'bucket' => 'your-s3-bucket'
+ }
+ }
+ ```
+
+1. Save the file and [reconfigure GitLab][] for the changes to take effect.
+
+---
+
+**Installations from source**
+
+Configuring the storage driver is done in your registry config YML file created
+when you [deployed your docker registry][registry-deploy].
+
+Example:
+
+```
+storage:
+ s3:
+ accesskey: 'AKIAKIAKI'
+ secretkey: 'secret123'
+ bucket: 'gitlab-registry-bucket-AKIAKIAKI'
+ cache:
+ blobdescriptor: inmemory
+ delete:
+ enabled: true
+```
+
## Storage limitations
Currently, there is no storage limitation, which means a user can upload an
diff --git a/doc/administration/raketasks/project_import_export.md b/doc/administration/raketasks/project_import_export.md
new file mode 100644
index 00000000000..c212059b9d5
--- /dev/null
+++ b/doc/administration/raketasks/project_import_export.md
@@ -0,0 +1,33 @@
+# Project import/export
+
+>**Note:**
+ - This feature was [introduced][ce-3050] in GitLab 8.9
+ - Importing will not be possible if the import instance version is lower
+ than that of the exporter.
+ - For existing installations, the project import option has to be enabled in
+ application settings (`/admin/application_settings`) under 'Import sources'.
+ - The exports are stored in a temporary [shared directory][tmp] and are deleted
+ every 24 hours by a specific worker.
+
+The GitLab Import/Export version can be checked by using:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake gitlab:import_export:version
+
+# Installations from source
+bundle exec rake gitlab:import_export:version RAILS_ENV=production
+```
+
+The current list of DB tables that will get exported can be listed by using:
+
+```bash
+# Omnibus installations
+sudo gitlab-rake gitlab:import_export:data
+
+# Installations from source
+bundle exec rake gitlab:import_export:data RAILS_ENV=production
+```
+
+[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[tmp]: ../../development/shared_files.md
diff --git a/doc/api/README.md b/doc/api/README.md
index 73f44603688..288f7f9ee69 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -32,6 +32,7 @@ following locations:
- [Services](services.md)
- [Session](session.md)
- [Settings](settings.md)
+- [Sidekiq metrics](sidekiq_metrics.md)
- [System Hooks](system_hooks.md)
- [Tags](tags.md)
- [Users](users.md)
diff --git a/doc/ci/README.md b/doc/ci/README.md
index ef72df97ce6..3dd4e2bc230 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -5,6 +5,8 @@
- [Get started with GitLab CI](quick_start/README.md)
- [CI examples for various languages](examples/README.md)
- [Learn how to enable or disable GitLab CI](enable_or_disable_ci.md)
+- [Pipelines and builds](pipelines.md)
+- [Environments and deployments](environments.md)
- [Learn how `.gitlab-ci.yml` works](yaml/README.md)
- [Configure a Runner, the application that runs your builds](runners/README.md)
- [Use Docker images with GitLab Runner](docker/using_docker_images.md)
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
new file mode 100644
index 00000000000..d85b8a34ced
--- /dev/null
+++ b/doc/ci/environments.md
@@ -0,0 +1,58 @@
+# Introduction to environments and deployments
+
+>**Note:**
+Introduced in GitLab 8.9.
+
+## Environments
+
+Environments are places where code gets deployed, such as staging or production.
+CI/CD [Pipelines] usually have one or more [jobs] that deploy to an environment.
+Defining environments in a project's `.gitlab-ci.yml` lets developers track
+[deployments] to these environments.
+
+## Deployments
+
+Deployments are created when [jobs] deploy versions of code to [environments].
+
+## Defining environments
+
+You can create and delete environments manually in the web interface, but we
+recommend that you define your environments in `.gitlab-ci.yml` first, which
+will automatically create environments for you after the first deploy.
+
+The `environment` is just a hint for GitLab that this job actually deploys to
+this environment. Each time the job succeeds, a deployment is recorded,
+remembering the git SHA and environment.
+
+Add something like this to your `.gitlab-ci.yml`:
+```
+production:
+ stage: deploy
+ script: dpl...
+ environment: production
+```
+
+See full [documentation](yaml/README.md#environment).
+
+## Seeing environment status
+
+You can find the environment list under **Pipelines > Environments** for your
+project. You'll see the git SHA and date of the last deployment to each
+environment defined.
+
+>**Note:**
+Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
+show up in the environments and deployments lists.
+
+## Seeing deployment history
+
+Clicking on an environment will show the history of deployments.
+
+>**Note:**
+Only deploys that happen after your `.gitlab-ci.yml` is properly configured will
+show up in the environments and deployments lists.
+
+[Pipelines]: pipelines.md
+[jobs]: yaml/README.md#jobs
+[environments]: #environments
+[deployments]: #deployments
diff --git a/doc/ci/examples/README.md b/doc/ci/examples/README.md
index 61294be599d..27bc21c2922 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -4,7 +4,8 @@
- [Test and deploy a Ruby application to Heroku](test-and-deploy-ruby-application-to-heroku.md)
- [Test and deploy a Python application to Heroku](test-and-deploy-python-application-to-heroku.md)
- [Test a Clojure application](test-clojure-application.md)
-- [Using `dpl` as deployment tool](../deployment/README.md)
+- [Test a Scala application](test-scala-application.md)
+- [Using `dpl` as deployment tool](deployment/README.md)
- Help your favorite programming language and GitLab by sending a merge request
with a guide for that language.
diff --git a/doc/ci/examples/test-scala-application.md b/doc/ci/examples/test-scala-application.md
new file mode 100644
index 00000000000..7412fdbbc78
--- /dev/null
+++ b/doc/ci/examples/test-scala-application.md
@@ -0,0 +1,47 @@
+## Test a Scala application
+
+This example demonstrates the integration of Gitlab CI with Scala
+applications using SBT. Checkout the example
+[project](https://gitlab.com/gitlab-examples/scala-sbt) and
+[build status](https://gitlab.com/gitlab-examples/scala-sbt/builds).
+
+### 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: java:8
+
+before_script:
+ - apt-get update -y
+ - apt-get install apt-transport-https -y
+ # Install SBT
+ - echo "deb http://dl.bintray.com/sbt/debian /" | tee -a /etc/apt/sources.list.d/sbt.list
+ - apt-key adv --keyserver hkp://keyserver.ubuntu.com:80 --recv 642AC823
+ - apt-get update -y
+ - apt-get install sbt -y
+ - sbt sbt-version
+
+test:
+ script:
+ - sbt clean coverage test coverageReport
+```
+
+The `before_script` installs [SBT](http://www.scala-sbt.org/) and
+displays the version that is being used. The `test` stage executes SBT
+to compile and test the project.
+[scoverage](https://github.com/scoverage/sbt-scoverage) is used as an SBT
+plugin to measure test coverage.
+
+You can use other versions of Scala and SBT by defining them in
+`build.sbt`.
+
+### Display test coverage in build
+
+Add the `Coverage was \[\d+.\d+\%\]` regular expression in the
+**Settings > Edit Project > Test coverage parsing** project setting to
+retrieve the test coverage rate from the build trace and have it
+displayed with your builds.
+
+**Builds** must be enabled for this option to appear.
diff --git a/doc/ci/pipelines.md b/doc/ci/pipelines.md
new file mode 100644
index 00000000000..48a9f994759
--- /dev/null
+++ b/doc/ci/pipelines.md
@@ -0,0 +1,38 @@
+# Introduction to pipelines and builds
+
+>**Note:**
+Introduced in GitLab 8.8.
+
+## Pipelines
+
+A pipeline is a group of [builds] that get executed in [stages] (batches). All
+of the builds in a stage are executed in parallel (if there are enough
+concurrent [runners]), and if they all succeed, the pipeline moves on to the
+next stage. If one of the builds fails, the next stage is not (usually)
+executed.
+
+## Builds
+
+Builds are individual runs of [jobs]. Not to be confused with a `build` job or
+`build` stage.
+
+## Defining pipelines
+
+Pipelines are defined in `.gitlab-ci.yml` by specifying [jobs] that run in
+[stages].
+
+See full [documentation](yaml/README.md#jobs).
+
+## Seeing pipeline status
+
+You can find the current and historical pipeline runs under **Pipelines** for your
+project.
+
+## Seeing build status
+
+Clicking on a pipeline will show the builds that were run for that pipeline.
+
+[builds]: #builds
+[jobs]: yaml/README.md#jobs
+[stages]: yaml/README.md#stages
+[runners]: runners/README.md
diff --git a/doc/ci/quick_start/README.md b/doc/ci/quick_start/README.md
index 386b8e29fcf..7fa1a478f34 100644
--- a/doc/ci/quick_start/README.md
+++ b/doc/ci/quick_start/README.md
@@ -4,41 +4,41 @@
is fully integrated into GitLab itself and is [enabled] by default on all
projects.
-The TL;DR version of how GitLab CI works is the following.
-
----
-
GitLab offers a [continuous integration][ci] service. If you
[add a `.gitlab-ci.yml` file][yaml] to the root directory of your repository,
and configure your GitLab project to use a [Runner], then each merge request or
-push triggers a build.
+push triggers your CI [pipeline].
-The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it
-runs three [stages]: `build`, `test`, and `deploy`.
+The `.gitlab-ci.yml` file tells the GitLab runner what to do. By default it runs
+a pipeline with three [stages]: `build`, `test`, and `deploy`. You don't need to
+use all three stages; stages with no jobs are simply ignored.
If everything runs OK (no non-zero return values), you'll get a nice green
checkmark associated with the pushed commit or merge request. This makes it
-easy to see whether a merge request will cause any of the tests to fail before
+easy to see whether a merge request caused any of the tests to fail before
you even look at the code.
-Most projects only use GitLab's CI service to run the test suite so that
+Most projects use GitLab's CI service to run the test suite so that
developers get immediate feedback if they broke something.
+There's a growing trend to use continuous delivery and continuous deployment to
+automatically deploy tested code to staging and production environments.
+
So in brief, the steps needed to have a working CI can be summed up to:
1. Add `.gitlab-ci.yml` to the root directory of your repository
1. Configure a Runner
-From there on, on every push to your Git repository, the build will be
-automagically started by the Runner and will appear under the project's
-`/builds` page.
+From there on, on every push to your Git repository, the Runner will
+automagically start the pipeline and the pipeline will appear under the
+project's `/pipelines` page.
---
This guide assumes that you:
- have a working GitLab instance of version 8.0 or higher or are using
- [GitLab.com](https://gitlab.com/users/sign_in)
+ [GitLab.com](https://gitlab.com)
- have a project in GitLab that you would like to use CI for
Let's break it down to pieces and work on solving the GitLab CI puzzle.
@@ -57,15 +57,14 @@ On any push to your repository, GitLab will look for the `.gitlab-ci.yml`
file and start builds on _Runners_ according to the contents of the file,
for that commit.
-Because `.gitlab-ci.yml` is in the repository, it is version controlled,
-old versions still build successfully, forks can easily make use of CI,
-branches can have separate builds and you have a single source of truth for CI.
-You can read more about the reasons why we are using `.gitlab-ci.yml`
-[in our blog about it][blog-ci].
+Because `.gitlab-ci.yml` is in the repository and is version controlled, old
+versions still build successfully, forks can easily make use of CI, branches can
+have different pipelines and jobs, and you have a single source of truth for CI.
+You can read more about the reasons why we are using `.gitlab-ci.yml` [in our
+blog about it][blog-ci].
**Note:** `.gitlab-ci.yml` is a [YAML](https://en.wikipedia.org/wiki/YAML) file
-so you have to pay extra attention to the indentation. Always use spaces, not
-tabs.
+so you have to pay extra attention to indentation. Always use spaces, not tabs.
### Creating a simple `.gitlab-ci.yml` file
@@ -108,7 +107,7 @@ If you want to check whether your `.gitlab-ci.yml` file is valid, there is a
Lint tool under the page `/ci/lint` of your GitLab instance. You can also find
the link under **Settings > CI settings** in your project.
-For more information and a complete `.gitlab-ci.yml` syntax, please check
+For more information and a complete `.gitlab-ci.yml` syntax, please read
[the documentation on .gitlab-ci.yml](../yaml/README.md).
### Push `.gitlab-ci.yml` to GitLab
@@ -122,7 +121,8 @@ git commit -m "Add .gitlab-ci.yml"
git push origin master
```
-Now if you go to the **Builds** page you will see that the builds are pending.
+Now if you go to the **Pipelines** page you will see that the pipeline is
+pending.
You can also go to the **Commits** page and notice the little clock icon next
to the commit SHA.
@@ -138,15 +138,14 @@ Notice that there are two jobs pending which are named after what we wrote in
`.gitlab-ci.yml`. The red triangle indicates that there is no Runner configured
yet for these builds.
-The next step is to configure a Runner so that it picks the pending jobs.
+The next step is to configure a Runner so that it picks the pending builds.
## Configuring a Runner
-In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`.
-A Runner can be a virtual machine, a VPS, a bare-metal machine, a docker
-container or even a cluster of containers. GitLab and the Runners communicate
-through an API, so the only needed requirement is that the machine on which the
-Runner is configured to have Internet access.
+In GitLab, Runners run the builds that you define in `.gitlab-ci.yml`. A Runner
+can be a virtual machine, a VPS, a bare-metal machine, a docker container or
+even a cluster of containers. GitLab and the Runners communicate through an API,
+so the only requirement is that the Runner's machine has Internet access.
A Runner can be specific to a certain project or serve multiple projects in
GitLab. If it serves all projects it's called a _Shared Runner_.
@@ -188,12 +187,16 @@ To enable **Shared Runners** you have to go to your project's
[Read more on Shared Runners](../runners/README.md).
-## Seeing the status of your build
+## Seeing the status of your pipeline and builds
After configuring the Runner successfully, you should see the status of your
last commit change from _pending_ to either _running_, _success_ or _failed_.
-You can view all builds, by going to the **Builds** page in your project.
+You can view all pipelines by going to the **Pipelines** page in your project.
+
+![Commit status](img/pipelines_status.png)
+
+Or you can view all builds, by going to the **Pipelines > Builds** page.
![Commit status](img/builds_status.png)
@@ -238,3 +241,4 @@ CI with various languages.
[runner]: ../runners/README.md
[enabled]: ../enable_or_disable_ci.md
[stages]: ../yaml/README.md#stages
+[pipeline]: ../pipelines.md
diff --git a/doc/ci/quick_start/img/pipelines_status.png b/doc/ci/quick_start/img/pipelines_status.png
new file mode 100644
index 00000000000..6bc97bb739c
--- /dev/null
+++ b/doc/ci/quick_start/img/pipelines_status.png
Binary files differ
diff --git a/doc/ci/runners/README.md b/doc/ci/runners/README.md
index 400784da617..ddebd987650 100644
--- a/doc/ci/runners/README.md
+++ b/doc/ci/runners/README.md
@@ -96,6 +96,12 @@ To register the runner, run the command below and follow instructions:
sudo gitlab-ci-multi-runner register
```
+### Lock a specific runner from being enabled for other projects
+
+You can configure a runner to assign it exclusively to a project. When a
+runner is locked this way, it can no longer be enabled for other projects.
+This setting is available on each runner in *Project Settings* > *Runners*.
+
### Making an existing Shared Runner Specific
If you are an admin on your GitLab instance,
@@ -128,7 +134,7 @@ the appropriate dependencies to run Rails test suites.
### Prevent runner with tags from picking jobs without tags
You can configure a runner to prevent it from picking jobs with tags when
-the runnner does not have tags assigned. This setting is available on each
+the runner does not have tags assigned. This setting is available on each
runner in *Project Settings* > *Runners*.
### Be careful with sensitive information
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 7d2afe4c3fa..ef9feb683ef 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -56,7 +56,7 @@ of your repository and contains definitions of how your project should be built.
The YAML file defines a set of jobs with constraints stating when they should
be run. The jobs are defined as top-level elements with a name and always have
-to contain the `script` clause:
+to contain at least the `script` clause:
```yaml
job1:
@@ -167,9 +167,9 @@ stages:
There are also two edge cases worth mentioning:
-1. If no `stages` is defined in `.gitlab-ci.yml`, then by default the `build`,
+1. If no `stages` are defined in `.gitlab-ci.yml`, then by default the `build`,
`test` and `deploy` are allowed to be used as job's stage by default.
-2. If a job doesn't specify `stage`, the job is assigned the `test` stage.
+2. If a job doesn't specify a `stage`, the job is assigned the `test` stage.
### types
@@ -180,9 +180,9 @@ Alias for [stages](#stages).
>**Note:**
Introduced in GitLab Runner v0.5.0.
-GitLab CI allows you to add to `.gitlab-ci.yml` variables that are set in build
-environment. The variables are stored in the git repository and are meant to
-store non-sensitive project configuration, for example:
+GitLab CI allows you to add variables to `.gitlab-ci.yml` that are set in the
+build environment. The variables are stored in the git repository and are meant
+to store non-sensitive project configuration, for example:
```yaml
variables:
@@ -255,8 +255,8 @@ rspec:
- binaries/
```
-The cache is provided on best effort basis, so don't expect that cache will be
-always present. For implementation details please check GitLab Runner.
+The cache is provided on a best-effort basis, so don't expect that the cache
+will be always present. For implementation details, please check GitLab Runner.
#### cache:key
@@ -481,10 +481,10 @@ failure.
`when` can be set to one of the following values:
1. `on_success` - execute build only when all builds from prior stages
- succeeded. This is the default.
+ succeed. This is the default.
1. `on_failure` - execute build only when at least one build from prior stages
- failed.
-1. `always` - execute build despite the status of builds from prior stages.
+ fails.
+1. `always` - execute build regardless of the status of builds from prior stages.
For example:
@@ -532,14 +532,18 @@ The above script will:
### environment
>**Note:**
-Introduced in GitLab v8.9.0.
+Introduced in GitLab 8.9.
-`environment` is used to define that job does deployment to specific environment.
-This allows to easily track all deployments to your environments straight from GitLab.
+`environment` is used to define that a job deploys to a specific environment.
+This allows easy tracking of all deployments to your environments straight from
+GitLab.
-If `environment` is specified and no environment under that name does exist a new one will be created automatically.
+If `environment` is specified and no environment under that name exists, a new
+one will be created automatically.
-The `environment` name must contain only letters, digits, '-' and '_'.
+The `environment` name must contain only letters, digits, '-' and '_'. Common
+names are `qa`, `staging`, and `production`, but you can use whatever name works
+with your workflow.
---
@@ -552,7 +556,8 @@ deploy to production:
environment: production
```
-The `deploy to production` job will be marked as doing deployment to `production` environment.
+The `deploy to production` job will be marked as doing deployment to
+`production` environment.
### artifacts
@@ -561,10 +566,10 @@ The `deploy to production` job will be marked as doing deployment to `production
> - Introduced in GitLab Runner v0.7.0 for non-Windows platforms.
> - Windows support was added in GitLab Runner v.1.0.0.
> - Currently not all executors are supported.
-> - Build artifacts are only collected for successful builds.
+> - Build artifacts are only collected for successful builds by default.
-`artifacts` is used to specify list of files and directories which should be
-attached to build after success. To pass artifacts between different builds,
+`artifacts` is used to specify a list of files and directories which should be
+attached to the build after success. To pass artifacts between different builds,
see [dependencies](#dependencies).
Below are some examples.
@@ -692,9 +697,9 @@ failure.
`artifacts:when` can be set to one of the following values:
-1. `on_success` - upload artifacts only when build succeeds. This is the default
-1. `on_failure` - upload artifacts only when build fails
-1. `always` - upload artifacts despite the build status
+1. `on_success` - upload artifacts only when the build succeeds. This is the default.
+1. `on_failure` - upload artifacts only when the build fails.
+1. `always` - upload artifacts regardless of the build status.
---
@@ -713,16 +718,18 @@ job:
>**Note:**
Introduced in GitLab 8.9 and GitLab Runner v1.3.0.
-`artifacts:expire_in` is used to remove uploaded artifacts after specified time.
-By default artifacts are stored on GitLab forver.
-`expire_in` allows to specify after what time the artifacts should be removed.
-The artifacts will expire counting from the moment when they are uploaded and stored on GitLab.
+`artifacts:expire_in` is used to delete uploaded artifacts after the specified
+time. By default, artifacts are stored on GitLab forever. `expire_in` allows you
+to specify how long artifacts should live before they expire, counting from the
+time they are uploaded and stored on GitLab.
-After artifacts uploading you can use the **Keep** button on build page to keep the artifacts forever.
+You can use the **Keep** button on the build page to override expiration and
+keep artifacts forever.
-Artifacts are removed every hour, but they are not accessible after expire date.
+By default, artifacts are deleted hourly (via a cron job), but they are not
+accessible after expiry.
-The value of `expire_in` is a elapsed time. The example of parsable values:
+The value of `expire_in` is an elapsed time. Examples of parseable values:
- '3 mins 4 sec'
- '2 hrs 20 min'
- '2h20min'
@@ -734,7 +741,7 @@ The value of `expire_in` is a elapsed time. The example of parsable values:
**Example configurations**
-To expire artifacts after 1 week from the moment that they are uploaded:
+To expire artifacts 1 week after being uploaded:
```yaml
job:
diff --git a/doc/development/doc_styleguide.md b/doc/development/doc_styleguide.md
index f5d97179f8a..975bb82c37d 100644
--- a/doc/development/doc_styleguide.md
+++ b/doc/development/doc_styleguide.md
@@ -183,6 +183,62 @@ For example, if you were to move `doc/workflow/lfs/lfs_administration.md` to
(`workflow/lfs/lfs_administration.md`).
+## Configuration documentation for source and Omnibus installations
+
+GitLab currently officially supports two installation methods: installations
+from source and Omnibus packages installations.
+
+Whenever there is a setting that is configurable for both installation methods,
+prefer to document it in the CE docs to avoid duplication.
+
+Configuration settings include:
+
+- settings that touch configuration files in `config/`
+- NGINX settings and settings in `lib/support/` in general
+
+When there is a list of steps to perform, usually that entails editing the
+configuration file and reconfiguring/restarting GitLab. In such case, follow
+the style below as a guide:
+
+````
+**For Omnibus installations**
+
+1. Edit `/etc/gitlab/gitlab.rb`:
+
+ ```ruby
+ external_url "https://gitlab.example.com"
+ ```
+
+1. Save the file and [reconfigure] GitLab for the changes to take effect.
+
+---
+
+**For installations from source**
+
+1. Edit `config/gitlab.yml`:
+
+ ```yaml
+ gitlab:
+ host: "gitlab.example.com"
+ ```
+
+1. Save the file and [restart] GitLab for the changes to take effect.
+
+
+[reconfigure]: path/to/administration/gitlab_restart.md#omnibus-gitlab-reconfigure
+[restart]: path/to/administration/gitlab_restart.md#installations-from-source
+````
+
+In this case:
+
+- before each step list the installation method is declared in bold
+- three dashes (`---`) are used to create an horizontal line and separate the
+ two methods
+- the code blocks are indented one or more spaces under the list item to render
+ correctly
+- different highlighting languages are used for each config in the code block
+- the [references](#references) guide is used for reconfigure/restart
+
## API
Here is a list of must-have items. Use them in the exact order that appears
diff --git a/doc/project_services/emails_on_push.md b/doc/project_services/emails_on_push.md
new file mode 100644
index 00000000000..2f9f36f962e
--- /dev/null
+++ b/doc/project_services/emails_on_push.md
@@ -0,0 +1,17 @@
+## Enabling emails on push
+
+To receive email notifications for every change that is pushed to the project, visit
+your project's **Settings > Services > Emails on push** and activate the service.
+
+In the _Recipients_ area, provide a list of emails separated by commas.
+
+You can configure any of the following settings depending on your preference.
+
++ **Push events** - Email will be triggered when a push event is recieved
++ **Tag push events** - Email will be triggered when a tag is created and pushed
++ **Send from committer** - Send notifications from the committer's email address if the domain is part of the domain GitLab is running on (e.g. `user@gitlab.com`).
++ **Disable code diffs** - Don't include possibly sensitive code diffs in notification body.
+
+---
+
+![Email on push service settings](img/emails_on_push_service.png)
diff --git a/doc/project_services/img/emails_on_push_service.png b/doc/project_services/img/emails_on_push_service.png
new file mode 100644
index 00000000000..cd6f79ad1eb
--- /dev/null
+++ b/doc/project_services/img/emails_on_push_service.png
Binary files differ
diff --git a/doc/project_services/project_services.md b/doc/project_services/project_services.md
index a5af620d9be..f81a035f70b 100644
--- a/doc/project_services/project_services.md
+++ b/doc/project_services/project_services.md
@@ -33,7 +33,7 @@ further configuration instructions and details. Contributions are welcome.
| Campfire | Simple web-based real-time group chat |
| Custom Issue Tracker | Custom issue tracker |
| Drone CI | Continuous Integration platform built on Docker, written in Go |
-| Emails on push | Email the commits and diff of each push to a list of recipients |
+| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
| Gemnasium | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
diff --git a/doc/update/8.6-to-8.7.md b/doc/update/8.6-to-8.7.md
index bb463d43a7c..cb66ef920bb 100644
--- a/doc/update/8.6-to-8.7.md
+++ b/doc/update/8.6-to-8.7.md
@@ -45,7 +45,7 @@ sudo -u git -H git checkout 8-7-stable-ee
```bash
cd /home/git/gitlab-shell
-sudo -u git -H git fetch --all --tags
+sudo -u git -H git fetch --tags
sudo -u git -H git checkout v2.7.2
```
diff --git a/doc/user/project/settings/img/import_export_download_export.png b/doc/user/project/settings/img/import_export_download_export.png
new file mode 100644
index 00000000000..a2f7f0085c1
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_download_export.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_export_button.png b/doc/user/project/settings/img/import_export_export_button.png
new file mode 100644
index 00000000000..1f7bdd21b0d
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_export_button.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_mail_link.png b/doc/user/project/settings/img/import_export_mail_link.png
new file mode 100644
index 00000000000..c123f83eb8e
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_mail_link.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_new_project.png b/doc/user/project/settings/img/import_export_new_project.png
new file mode 100644
index 00000000000..b3a7f201018
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_new_project.png
Binary files differ
diff --git a/doc/user/project/settings/img/import_export_select_file.png b/doc/user/project/settings/img/import_export_select_file.png
new file mode 100644
index 00000000000..f31832af3e1
--- /dev/null
+++ b/doc/user/project/settings/img/import_export_select_file.png
Binary files differ
diff --git a/doc/user/project/settings/img/settings_edit_button.png b/doc/user/project/settings/img/settings_edit_button.png
new file mode 100644
index 00000000000..3c0cee536de
--- /dev/null
+++ b/doc/user/project/settings/img/settings_edit_button.png
Binary files differ
diff --git a/doc/user/project/settings/import_export.md b/doc/user/project/settings/import_export.md
new file mode 100644
index 00000000000..38e9786123d
--- /dev/null
+++ b/doc/user/project/settings/import_export.md
@@ -0,0 +1,73 @@
+# Project import/export
+
+>**Notes:**
+ - This feature was [introduced][ce-3050] in GitLab 8.9
+ - Importing will not be possible if the import instance version is lower
+ than that of the exporter.
+ - For existing installations, the project import option has to be enabled in
+ application settings (`/admin/application_settings`) under 'Import sources'.
+ 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.
+ - The exports are stored in a temporary [shared directory][tmp] and are deleted
+ every 24 hours by a specific worker.
+
+Existing projects running on any GitLab instance or GitLab.com can be exported
+with all their related data and be moved into a new GitLab instance.
+
+## Exported contents
+
+The following items will be exported:
+
+- Project and wiki repositories
+- Project uploads
+- Project configuration including web hooks and services
+- Issues with comments, merge requests with diffs and comments, labels, milestones, snippets,
+ and other project entities
+
+The following items will NOT be exported:
+
+- Build traces and artifacts
+- LFS objects
+
+## Exporting a project and its data
+
+1. Go to the project settings page by clicking on **Edit Project**:
+
+ ![Project settings button](img/settings_edit_button.png)
+
+1. Scroll down to find the **Export project** button:
+
+ ![Export button](img/import_export_export_button.png)
+
+1. Once the export is generated, you should receive an e-mail with a link to
+ download the file:
+
+ ![Email download link](img/import_export_mail_link.png)
+
+1. Alternatively, you can come back to the project settings and download the
+ file from there, or generate a new export. Once the file available, the page
+ should show the **Download export** button:
+
+ ![Download export](img/import_export_download_export.png)
+
+## Importing the project
+
+1. The new GitLab project import feature is at the far right of the import
+ options when creating a New Project. Make sure you are in the right namespace
+ and you have entered a project name. Click on **GitLab export**:
+
+ ![New project](img/import_export_new_project.png)
+
+1. You can see where the project will be imported to. You can now select file
+ exported previously:
+
+ ![Select file](img/import_export_select_file.png)
+
+1. Click on **Import project** to begin importing. Your newly imported project
+ page will appear soon.
+
+[ce-3050]: https://gitlab.com/gitlab-org/gitlab-ce/issues/3050
+[tmp]: ../../../development/shared_files.md
diff --git a/doc/workflow/add-user/add-user.md b/doc/workflow/add-user/add-user.md
index fffa0aba57f..4b551130255 100644
--- a/doc/workflow/add-user/add-user.md
+++ b/doc/workflow/add-user/add-user.md
@@ -8,7 +8,7 @@ You should have `master` or `owner` permissions to add or import a new user
to your project.
The first step to add or import a user, go to your project and click on
-**Members** on the left side of your screen.
+**Members** in the drop-down menu on the right side of your screen.
![Members](img/add_user_members_menu.png)
@@ -87,3 +87,25 @@ invitation, change their access level or even delete them.
Once the user accepts the invitation, they will be prompted to create a new
GitLab account using the same e-mail address the invitation was sent to.
+
+## Request access to a project
+
+As a user, you can request to be a member of a project. Go to the project you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](img/request_access_button.png)
+
+---
+
+Project owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](img/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](img/withdraw_access_request_button.png)
diff --git a/doc/workflow/add-user/img/access_requests_management.png b/doc/workflow/add-user/img/access_requests_management.png
new file mode 100644
index 00000000000..e9641cb4f85
--- /dev/null
+++ b/doc/workflow/add-user/img/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_accept.png b/doc/workflow/add-user/img/add_user_email_accept.png
index 910affc9659..18aabf93d50 100644
--- a/doc/workflow/add-user/img/add_user_email_accept.png
+++ b/doc/workflow/add-user/img/add_user_email_accept.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_ready.png b/doc/workflow/add-user/img/add_user_email_ready.png
index 5f02ce89b3e..385d64330c0 100644
--- a/doc/workflow/add-user/img/add_user_email_ready.png
+++ b/doc/workflow/add-user/img/add_user_email_ready.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_email_search.png b/doc/workflow/add-user/img/add_user_email_search.png
index 140979fbe13..84741edbca4 100644
--- a/doc/workflow/add-user/img/add_user_email_search.png
+++ b/doc/workflow/add-user/img/add_user_email_search.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_give_permissions.png b/doc/workflow/add-user/img/add_user_give_permissions.png
index 8ef9156c8d5..7e580384e54 100644
--- a/doc/workflow/add-user/img/add_user_give_permissions.png
+++ b/doc/workflow/add-user/img/add_user_give_permissions.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
index 5770d5cf0c4..8dbd73a5bc8 100644
--- a/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
+++ b/doc/workflow/add-user/img/add_user_import_members_from_another_project.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_imported_members.png b/doc/workflow/add-user/img/add_user_imported_members.png
index dea4b3f40ad..abac1f59c02 100644
--- a/doc/workflow/add-user/img/add_user_imported_members.png
+++ b/doc/workflow/add-user/img/add_user_imported_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_list_members.png b/doc/workflow/add-user/img/add_user_list_members.png
index 7daa6ca7d9e..e17d88c6f5f 100644
--- a/doc/workflow/add-user/img/add_user_list_members.png
+++ b/doc/workflow/add-user/img/add_user_list_members.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_members_menu.png b/doc/workflow/add-user/img/add_user_members_menu.png
index f1797b95f67..ec5d39f402d 100644
--- a/doc/workflow/add-user/img/add_user_members_menu.png
+++ b/doc/workflow/add-user/img/add_user_members_menu.png
Binary files differ
diff --git a/doc/workflow/add-user/img/add_user_search_people.png b/doc/workflow/add-user/img/add_user_search_people.png
index 5ac10ce80d4..eaa062376f4 100644
--- a/doc/workflow/add-user/img/add_user_search_people.png
+++ b/doc/workflow/add-user/img/add_user_search_people.png
Binary files differ
diff --git a/doc/workflow/add-user/img/request_access_button.png b/doc/workflow/add-user/img/request_access_button.png
new file mode 100644
index 00000000000..984d640b0f0
--- /dev/null
+++ b/doc/workflow/add-user/img/request_access_button.png
Binary files differ
diff --git a/doc/workflow/add-user/img/withdraw_access_request_button.png b/doc/workflow/add-user/img/withdraw_access_request_button.png
new file mode 100644
index 00000000000..ff54a0e4384
--- /dev/null
+++ b/doc/workflow/add-user/img/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/award_emoji.md b/doc/workflow/award_emoji.md
index 70b35c58be6..e6f8b792707 100644
--- a/doc/workflow/award_emoji.md
+++ b/doc/workflow/award_emoji.md
@@ -1,28 +1,26 @@
-# Award emojis
+# Award emoji
>**Note:**
This feature was [introduced][1825] in GitLab 8.2.
When you're collaborating online, you get fewer opportunities for high-fives
-and thumbs-ups. In order to make virtual celebrations easier, you can now vote
-on issues and merge requests using emoji!
+and thumbs-ups. Emoji can be awarded to issues and merge requests, making
+virtual celebrations easier.
![Award emoji](img/award_emoji_select.png)
-This makes it much easier to give and receive feedback, without a long comment
-thread. Any comment that contains only the thumbs up or down emojis is
-converted to a vote and depicted in the emoji area.
-
-You can then use that functionality to sort issues and merge requests based on
-popularity.
+Award emoji make it much easier to give and receive feedback without a long
+comment thread. Comments that are only emoji will automatically become
+award emoji.
## Sort issues and merge requests on vote count
>**Note:**
This feature was [introduced][2871] in GitLab 8.5.
-You can quickly sort the issues or merge requests by the number of votes they
-have received. The sort option can be found in the right dropdown menu.
+You can quickly sort issues and merge requests by the number of votes they
+have received. The sort options can be found in the dropdown menu as "Most
+popular" and "Least popular".
![Votes sort options](img/award_emoji_votes_sort_options.png)
@@ -40,9 +38,28 @@ Sort by least popular issues/merge requests.
---
-The number of upvotes and downvotes is not summed up. That means that an issue
-with 18 upvotes and 5 downvotes is considered more popular than an issue with
-17 upvotes and no downvotes.
+The total number of votes is not summed up. An issue with 18 upvotes and 5
+downvotes is considered more popular than an issue with 17 upvotes and no
+downvotes.
+
+## Award emoji for comments
+
+>**Note:**
+This feature was [introduced][4291] in GitLab 8.9.
+
+Award emoji can also be applied to individual comments when you want to
+celebrate an accomplishment or agree with an opinion.
+
+To add an award emoji, click the smile in the top right of the comment and pick
+an emoji from the dropdown.
+
+![Picking an emoji for a comment](img/award_emoji_comment_picker.png)
+
+![An award emoji has been applied to a comment](img/award_emoji_comment_awarded.png)
+
+If you want to remove an award emoji, just click the emoji again and the vote
+will be removed.
[2871]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/2781
[1825]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/1825
+[4291]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/4291
diff --git a/doc/workflow/award_emoji.png b/doc/workflow/award_emoji.png
index fb26ee04393..3408ed95841 100644
--- a/doc/workflow/award_emoji.png
+++ b/doc/workflow/award_emoji.png
Binary files differ
diff --git a/doc/workflow/groups.md b/doc/workflow/groups.md
index 34ada1774d8..1a316e80976 100644
--- a/doc/workflow/groups.md
+++ b/doc/workflow/groups.md
@@ -51,6 +51,28 @@ If necessary, you can increase the access level of an individual user for a spec
![Barry effectively has 'Master' access to GitLab CI now](groups/override_access_level.png)
+## Request access to a group
+
+As a user, you can request to be a member of a group. Go to the group you'd
+like to be a member of, and click the **Request Access** button on the right
+side of your screen.
+
+![Request access button](groups/request_access_button.png)
+
+---
+
+Group owners & masters will be notified of your request and will be able to approve or
+decline it on the members page.
+
+![Manage access requests](groups/access_requests_management.png)
+
+---
+
+If you change your mind before your request is approved, just click the
+**Withdraw Access Request** button.
+
+![Withdraw access request button](groups/withdraw_access_request_button.png)
+
## Managing group memberships via LDAP
In GitLab Enterprise Edition it is possible to manage GitLab group memberships using LDAP groups.
diff --git a/doc/workflow/groups/access_requests_management.png b/doc/workflow/groups/access_requests_management.png
new file mode 100644
index 00000000000..ffede8e9bd6
--- /dev/null
+++ b/doc/workflow/groups/access_requests_management.png
Binary files differ
diff --git a/doc/workflow/groups/request_access_button.png b/doc/workflow/groups/request_access_button.png
new file mode 100644
index 00000000000..ff0ac8747a7
--- /dev/null
+++ b/doc/workflow/groups/request_access_button.png
Binary files differ
diff --git a/doc/workflow/groups/withdraw_access_request_button.png b/doc/workflow/groups/withdraw_access_request_button.png
new file mode 100644
index 00000000000..99d7a326ed8
--- /dev/null
+++ b/doc/workflow/groups/withdraw_access_request_button.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_awarded.png b/doc/workflow/img/award_emoji_comment_awarded.png
new file mode 100644
index 00000000000..67697831869
--- /dev/null
+++ b/doc/workflow/img/award_emoji_comment_awarded.png
Binary files differ
diff --git a/doc/workflow/img/award_emoji_comment_picker.png b/doc/workflow/img/award_emoji_comment_picker.png
new file mode 100644
index 00000000000..d9c3faecdca
--- /dev/null
+++ b/doc/workflow/img/award_emoji_comment_picker.png
Binary files differ
diff --git a/doc/workflow/notifications.md b/doc/workflow/notifications.md
index cbca94c0b5e..fe4485e148a 100644
--- a/doc/workflow/notifications.md
+++ b/doc/workflow/notifications.md
@@ -4,7 +4,7 @@ GitLab has a notification system in place to notify a user of events that are im
## Notification settings
-Under user profile page you can find the notification settings.
+You can find notification settings under the user profile.
![notification settings](notifications/settings.png)
@@ -20,6 +20,7 @@ Each of these settings have levels of notification:
* Participating - receive notifications from related resources
* Watch - receive notifications from projects or groups user is a member of
* Global - notifications as set at the global settings
+* Custom - user will receive notifications when mentioned, is participant and custom selected events.
#### Global Settings
@@ -55,7 +56,7 @@ Below is the table of events users can be notified of:
| User added to project | User | Sent when user is added to project |
| Project access level changed | User | Sent when user project access level is changed |
| User added to group | User | Sent when user is added to group |
-| Group access level changed | User | Sent when user group access level is changed |
+| Group access level changed | User | Sent when user group access level is changed |
| Project moved | Project members [1] | [1] not disabled |
### Issue / Merge Request events
@@ -71,6 +72,7 @@ In all of the below cases, the notification will be sent to:
- Watchers: users with notification level "Watch"
- Subscribers: anyone who manually subscribed to the issue/merge request
+- Custom: Users with notification level "custom" who turned on notifications for any of the events present in the table below
| Event | Sent to |
|------------------------|---------|
diff --git a/doc/workflow/notifications/settings.png b/doc/workflow/notifications/settings.png
index e5b50ee2494..7c6857aad1a 100644
--- a/doc/workflow/notifications/settings.png
+++ b/doc/workflow/notifications/settings.png
Binary files differ
diff --git a/doc/workflow/shortcuts.png b/doc/workflow/shortcuts.png
index beb6c53ec77..16be0413b64 100644
--- a/doc/workflow/shortcuts.png
+++ b/doc/workflow/shortcuts.png
Binary files differ
diff --git a/features/admin/groups.feature b/features/admin/groups.feature
index ab7de7ac315..657e847cf4a 100644
--- a/features/admin/groups.feature
+++ b/features/admin/groups.feature
@@ -27,13 +27,6 @@ Feature: Admin Groups
Then I should see project shared with group
@javascript
- Scenario: Remove user from group
- Given we have user "John Doe" in group
- When I visit admin group page
- And I remove user "John Doe" from group
- Then I should not see "John Doe" in team list
-
- @javascript
Scenario: Invite user to a group by e-mail
When I visit admin group page
When I select user "johndoe@gitlab.com" from user list as "Reporter"
diff --git a/features/dashboard/group.feature b/features/dashboard/group.feature
index e3c01db2ebb..3ae2c679dc1 100644
--- a/features/dashboard/group.feature
+++ b/features/dashboard/group.feature
@@ -5,53 +5,9 @@ Feature: Dashboard Group
And "John Doe" is owner of group "Owned"
And "John Doe" is guest of group "Guest"
- # Leave groups
-
- @javascript
- Scenario: Owner should be able to leave from group if he is not the last owner
- Given "Mary Jane" is owner of group "Owned"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Owned"
- And I visit dashboard groups page
- Then I should not see group "Owned" in group list
- Then I should see group "Guest" in group list
-
- @javascript
- Scenario: Owner should not be able to leave from group if he is the last owner
- Given "Mary Jane" is guest of group "Owned"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Owned"
- Then I should see the "Can not leave message"
-
- @javascript
- Scenario: Guest should be able to leave from group
- Given "Mary Jane" is guest of group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should not see group "Guest" in group list
-
- @javascript
- Scenario: Guest should be able to leave from group even if he is the only user in the group
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should see group "Guest" in group list
- When I click on the "Leave" button for group "Guest"
- When I visit dashboard groups page
- Then I should see group "Owned" in group list
- Then I should not see group "Guest" in group list
-
Scenario: Create a group from dasboard
And I visit dashboard groups page
And I click new group link
And submit form with new group "Samurai" info
Then I should be redirected to group "Samurai" page
And I should see newly created group "Samurai"
-
diff --git a/features/project/active_tab.feature b/features/project/active_tab.feature
index c4f987a7923..57dda9c2234 100644
--- a/features/project/active_tab.feature
+++ b/features/project/active_tab.feature
@@ -10,9 +10,9 @@ Feature: Project Active Tab
Then the active main tab should be Home
And no other main tabs should be active
- Scenario: On Project Code
+ Scenario: On Project Repository
Given I visit my project's files page
- Then the active main tab should be Code
+ Then the active main tab should be Repository
And no other main tabs should be active
Scenario: On Project Issues
@@ -59,46 +59,46 @@ Feature: Project Active Tab
And no other sub navs should be active
And the active main tab should be Settings
- # Sub Tabs: Code
+ # Sub Tabs: Repository
- Scenario: On Project Code/Files
+ Scenario: On Project Repository/Files
Given I visit my project's files page
Then the active sub tab should be Files
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Commits
+ Scenario: On Project Repository/Commits
Given I visit my project's commits page
Then the active sub tab should be Commits
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Network
+ Scenario: On Project Repository/Network
Given I visit my project's network page
Then the active sub tab should be Network
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Compare
+ Scenario: On Project Repository/Compare
Given I visit my project's commits page
And I click the "Compare" tab
Then the active sub tab should be Compare
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Branches
+ Scenario: On Project Repository/Branches
Given I visit my project's commits page
And I click the "Branches" tab
Then the active sub tab should be Branches
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
- Scenario: On Project Code/Tags
+ Scenario: On Project Repository/Tags
Given I visit my project's commits page
And I click the "Tags" tab
Then the active sub tab should be Tags
And no other sub tabs should be active
- And the active main tab should be Code
+ And the active main tab should be Repository
Scenario: On Project Issues/Browse
Given I visit my project's issues page
diff --git a/features/project/shortcuts.feature b/features/project/shortcuts.feature
index c73d0b32337..f71f69ef060 100644
--- a/features/project/shortcuts.feature
+++ b/features/project/shortcuts.feature
@@ -8,21 +8,21 @@ Feature: Project Shortcuts
@javascript
Scenario: Navigate to files tab
Given I press "g" and "f"
- Then the active main tab should be Code
+ Then the active main tab should be Repository
Then the active sub tab should be Files
@javascript
Scenario: Navigate to commits tab
Given I visit my project's files page
Given I press "g" and "c"
- Then the active main tab should be Code
+ Then the active main tab should be Repository
Then the active sub tab should be Commits
@javascript
Scenario: Navigate to network tab
Given I press "g" and "n"
Then the active sub tab should be Network
- And the active main tab should be Code
+ And the active main tab should be Repository
@javascript
Scenario: Navigate to graphs tab
diff --git a/features/steps/admin/groups.rb b/features/steps/admin/groups.rb
index e1f1db2872f..8613dc537cc 100644
--- a/features/steps/admin/groups.rb
+++ b/features/steps/admin/groups.rb
@@ -62,7 +62,7 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
step 'I should see "johndoe@gitlab.com" in team list in every project as "Reporter"' do
page.within ".group-users-list" do
- expect(page).to have_content "johndoe@gitlab.com (invited)"
+ expect(page).to have_content "johndoe@gitlab.com – Invited by"
expect(page).to have_content "Reporter"
end
end
@@ -92,12 +92,6 @@ class Spinach::Features::AdminGroups < Spinach::FeatureSteps
current_group.add_reporter(user_john)
end
- step 'I remove user "John Doe" from group' do
- page.within "#user_#{user_john.id}" do
- click_link 'Remove user from group'
- end
- end
-
step 'I should not see "John Doe" in team list' do
page.within ".group-users-list" do
expect(page).not_to have_content "John Doe"
diff --git a/features/steps/dashboard/group.rb b/features/steps/dashboard/group.rb
index 9b79a3be49b..cf679fea530 100644
--- a/features/steps/dashboard/group.rb
+++ b/features/steps/dashboard/group.rb
@@ -4,44 +4,6 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
include SharedPaths
include SharedUser
- # Leave
-
- step 'I click on the "Leave" button for group "Owned"' do
- find(:css, 'li', text: "Owner").find(:css, 'i.fa.fa-sign-out').click
- # poltergeist always confirms popups.
- end
-
- step 'I click on the "Leave" button for group "Guest"' do
- find(:css, 'li', text: "Guest").find(:css, 'i.fa.fa-sign-out').click
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Leave" button for group "Owned"' do
- expect(find(:css, 'li', text: "Owner")).not_to have_selector(:css, 'i.fa.fa-sign-out')
- # poltergeist always confirms popups.
- end
-
- step 'I should not see the "Leave" button for groupr "Guest"' do
- expect(find(:css, 'li', text: "Guest")).not_to have_selector(:css, 'i.fa.fa-sign-out')
- # poltergeist always confirms popups.
- end
-
- step 'I should see group "Owned" in group list' do
- expect(page).to have_content("Owned")
- end
-
- step 'I should not see group "Owned" in group list' do
- expect(page).not_to have_content("Owned")
- end
-
- step 'I should see group "Guest" in group list' do
- expect(page).to have_content("Guest")
- end
-
- step 'I should not see group "Guest" in group list' do
- expect(page).not_to have_content("Guest")
- end
-
step 'I click new group link' do
click_link "New Group"
end
@@ -60,8 +22,4 @@ class Spinach::Features::DashboardGroup < Spinach::FeatureSteps
expect(page).to have_content "Samurai"
expect(page).to have_content "Tokugawa Shogunate"
end
-
- step 'I should see the "Can not leave message"' do
- expect(page).to have_content "You can not leave the \"Owned\" group."
- end
end
diff --git a/features/steps/profile/notifications.rb b/features/steps/profile/notifications.rb
index a96f35ada51..7e339443b75 100644
--- a/features/steps/profile/notifications.rb
+++ b/features/steps/profile/notifications.rb
@@ -11,12 +11,10 @@ class Spinach::Features::ProfileNotifications < Spinach::FeatureSteps
end
step 'I select Mention setting from dropdown' do
- select 'mention', from: 'notification_setting_level'
+ first(:link, "On mention").trigger('click')
end
step 'I should see Notification saved message' do
- page.within '.flash-container' do
- expect(page).to have_content 'Notification settings saved'
- end
+ expect(page).to have_content 'On mention'
end
end
diff --git a/features/steps/project/network_graph.rb b/features/steps/project/network_graph.rb
index 9b59b682676..019b3124a86 100644
--- a/features/steps/project/network_graph.rb
+++ b/features/steps/project/network_graph.rb
@@ -20,11 +20,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should select "master" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "master"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "master"
end
step 'page should select "v1.0.0" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "v1.0.0"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0"
end
step 'page should have "master" on graph' do
@@ -40,11 +40,19 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
When 'I switch ref to "feature"' do
- select 'feature', from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'feature'
+ end
end
When 'I switch ref to "v1.0.0"' do
- select 'v1.0.0', from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'v1.0.0'
+ end
end
When 'click "Show only selected branch" checkbox' do
@@ -68,11 +76,11 @@ class Spinach::Features::ProjectNetworkGraph < Spinach::FeatureSteps
end
step 'page should select "feature" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "feature"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "feature"
end
step 'page should select "v1.0.0" in select box' do
- expect(page).to have_selector '.select2-chosen', text: "v1.0.0"
+ expect(page).to have_selector '.dropdown-menu-toggle', text: "v1.0.0"
end
step 'page should have "feature" on graph' do
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 2a1a8e776f0..76fefee9254 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -126,7 +126,7 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I click notifications drop down button' do
- find('#notifications-button').click
+ first('.notifications-btn').click
end
step 'I choose Mention setting' do
@@ -134,8 +134,8 @@ class Spinach::Features::Project < Spinach::FeatureSteps
end
step 'I should see Notification saved message' do
- page.within '.flash-container' do
- expect(page).to have_content 'Notification settings saved'
+ page.within '#notifications-button' do
+ expect(page).to have_content 'On mention'
end
end
diff --git a/features/steps/project/project_find_file.rb b/features/steps/project/project_find_file.rb
index 47de4b91df1..90771847909 100644
--- a/features/steps/project/project_find_file.rb
+++ b/features/steps/project/project_find_file.rb
@@ -13,12 +13,12 @@ class Spinach::Features::ProjectFindFile < Spinach::FeatureSteps
end
step 'I should see "find file" page' do
- ensure_active_main_tab('Code')
+ ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
step 'I fill in Find by path with "git"' do
- ensure_active_main_tab('Code')
+ ensure_active_main_tab('Repository')
expect(page).to have_selector('.file-finder-holder', count: 1)
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 79a3ed8197e..0fe046dcbf6 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -290,15 +290,23 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
end
step "I switch ref to 'test'" do
- select "'test'", from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'test'
+ end
end
step "I switch ref to fix" do
- select "fix", from: 'ref'
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'fix'
+ end
end
step "I see the ref 'test' has been selected" do
- expect(page).to have_selector '.select2-chosen', text: "'test'"
+ expect(page).to have_selector '.dropdown-toggle-text', text: "'test'"
end
step "I visit the 'test' tree" do
diff --git a/features/steps/shared/project_tab.rb b/features/steps/shared/project_tab.rb
index bfee8793301..d6024212601 100644
--- a/features/steps/shared/project_tab.rb
+++ b/features/steps/shared/project_tab.rb
@@ -8,8 +8,8 @@ module SharedProjectTab
ensure_active_main_tab('Project')
end
- step 'the active main tab should be Code' do
- ensure_active_main_tab('Code')
+ step 'the active main tab should be Repository' do
+ ensure_active_main_tab('Repository')
end
step 'the active main tab should be Graphs' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index 0e7a1cc2623..f8f680a6311 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -33,7 +33,6 @@ module API
mount ::API::Commits
mount ::API::DeployKeys
mount ::API::Files
- mount ::API::Gitignores
mount ::API::GroupMembers
mount ::API::Groups
mount ::API::Internal
@@ -58,6 +57,7 @@ module API
mount ::API::Subscriptions
mount ::API::SystemHooks
mount ::API::Tags
+ mount ::API::Templates
mount ::API::Triggers
mount ::API::Users
mount ::API::Variables
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 2e397643ed1..5a23a18fe9c 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -423,6 +423,7 @@ module API
class RunnerDetails < Runner
expose :tag_list
expose :run_untagged
+ expose :locked
expose :version, :revision, :platform, :architecture
expose :contacted_at
expose :token, if: lambda { |runner, options| options[:current_user].is_admin? || !runner.is_shared? }
@@ -444,11 +445,7 @@ module API
expose :created_at, :started_at, :finished_at
expose :user, with: User
expose :artifacts_file, using: BuildArtifactFile, if: -> (build, opts) { build.artifacts? }
- expose :commit, with: RepoCommit do |repo_obj, _options|
- if repo_obj.respond_to?(:commit)
- repo_obj.commit.commit_data
- end
- end
+ expose :commit, with: RepoCommit
expose :runner, with: Runner
end
@@ -472,11 +469,11 @@ module API
expose :content
end
- class GitignoresList < Grape::Entity
+ class TemplatesList < Grape::Entity
expose :name
end
- class Gitignore < Grape::Entity
+ class Template < Grape::Entity
expose :name, :content
end
end
diff --git a/lib/api/gitignores.rb b/lib/api/gitignores.rb
deleted file mode 100644
index 270c9501dd2..00000000000
--- a/lib/api/gitignores.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-module API
- class Gitignores < Grape::API
-
- # Get the list of the available gitignore templates
- #
- # Example Request:
- # GET /gitignores
- get 'gitignores' do
- present Gitlab::Gitignore.all, with: Entities::GitignoresList
- end
-
- # Get the text for a specific gitignore
- #
- # Parameters:
- # name (required) - The name of a license
- #
- # Example Request:
- # GET /gitignores/Elixir
- #
- get 'gitignores/:name' do
- required_attributes! [:name]
-
- gitignore = Gitlab::Gitignore.find(params[:name])
- not_found!('.gitignore') unless gitignore
-
- present gitignore, with: Entities::Gitignore
- end
- end
-end
diff --git a/lib/api/internal.rb b/lib/api/internal.rb
index 3ac7b50c4ce..1d361569d59 100644
--- a/lib/api/internal.rb
+++ b/lib/api/internal.rb
@@ -23,8 +23,6 @@ module API
end
post "/allowed" do
- Gitlab::Metrics.action = 'Grape#/internal/allowed'
-
status 200
actor =
diff --git a/lib/api/runners.rb b/lib/api/runners.rb
index 4faba9dc87b..ecc8f2fc5a2 100644
--- a/lib/api/runners.rb
+++ b/lib/api/runners.rb
@@ -49,7 +49,7 @@ module API
runner = get_runner(params[:id])
authenticate_update_runner!(runner)
- attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged]
+ attrs = attributes_for_keys [:description, :active, :tag_list, :run_untagged, :locked]
if runner.update(attrs)
present runner, with: Entities::RunnerDetails, current_user: current_user
else
@@ -96,9 +96,14 @@ module API
runner = get_runner(params[:runner_id])
authenticate_enable_runner!(runner)
- Ci::RunnerProject.create(runner: runner, project: user_project)
- present runner, with: Entities::Runner
+ runner_project = runner.assign_to(user_project)
+
+ if runner_project.persisted?
+ present runner, with: Entities::Runner
+ else
+ conflict!("Runner was already enabled for this project")
+ end
end
# Disable project's runner
@@ -163,6 +168,7 @@ module API
def authenticate_enable_runner!(runner)
forbidden!("Runner is shared") if runner.is_shared?
+ forbidden!("Runner is locked") if runner.locked?
return if current_user.is_admin?
forbidden!("No access granted") unless user_can_access_runner?(runner)
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
new file mode 100644
index 00000000000..18408797756
--- /dev/null
+++ b/lib/api/templates.rb
@@ -0,0 +1,36 @@
+module API
+ class Templates < Grape::API
+ TEMPLATE_TYPES = {
+ gitignores: Gitlab::Template::Gitignore,
+ gitlab_ci_ymls: Gitlab::Template::GitlabCiYml
+ }.freeze
+
+ TEMPLATE_TYPES.each do |template, klass|
+ # Get the list of the available template
+ #
+ # Example Request:
+ # GET /gitignores
+ # GET /gitlab_ci_ymls
+ get template.to_s do
+ present klass.all, with: Entities::TemplatesList
+ end
+
+ # Get the text for a specific template
+ #
+ # Parameters:
+ # name (required) - The name of a template
+ #
+ # Example Request:
+ # GET /gitignores/Elixir
+ # GET /gitlab_ci_ymls/Ruby
+ get "#{template}/:name" do
+ required_attributes! [:name]
+
+ new_template = klass.find(params[:name])
+ not_found!(template.to_s.singularize) unless new_template
+
+ present new_template, with: Entities::Template
+ end
+ end
+ end
+end
diff --git a/lib/banzai.rb b/lib/banzai.rb
index b467413a7dd..093382261ae 100644
--- a/lib/banzai.rb
+++ b/lib/banzai.rb
@@ -7,10 +7,6 @@ module Banzai
Renderer.render_result(text, context)
end
- def self.pre_process(text, context)
- Renderer.pre_process(text, context)
- end
-
def self.post_process(html, context)
Renderer.post_process(html, context)
end
diff --git a/lib/banzai/filter/abstract_reference_filter.rb b/lib/banzai/filter/abstract_reference_filter.rb
index 4815bafe238..81d66271136 100644
--- a/lib/banzai/filter/abstract_reference_filter.rb
+++ b/lib/banzai/filter/abstract_reference_filter.rb
@@ -218,8 +218,9 @@ module Banzai
nodes.each do |node|
node.to_html.scan(regex) do
project = $~[:project] || current_project_path
+ symbol = $~[object_sym]
- refs[project] << $~[object_sym]
+ refs[project] << symbol if object_class.reference_valid?(symbol)
end
end
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index ea21c7b041c..c78da404607 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -14,6 +14,8 @@ module Banzai
def call
return doc unless linkable_files?
+ @uri_types = {}
+
doc.search('a:not(.gfm)').each do |el|
process_link_attr el.attribute('href')
end
@@ -48,7 +50,7 @@ module Banzai
uri.path = [
relative_url_root,
context[:project].path_with_namespace,
- path_type(file_path),
+ uri_type(file_path),
ref || context[:project].default_branch, # if no ref exists, point to the default branch
file_path
].compact.join('/').squeeze('/').chomp('/')
@@ -87,7 +89,7 @@ module Banzai
return path unless request_path
parts = request_path.split('/')
- parts.pop if path_type(request_path) != 'tree'
+ parts.pop if uri_type(request_path) != :tree
while path.start_with?('../')
parts.pop
@@ -98,45 +100,20 @@ module Banzai
end
def file_exists?(path)
- return false if path.nil?
- repository.blob_at(current_sha, path).present? ||
- repository.tree(current_sha, path).entries.any?
- end
-
- # Get the type of the given path
- #
- # path - String path to check
- #
- # Examples:
- #
- # path_type('doc/README.md') # => 'blob'
- # path_type('doc/logo.png') # => 'raw'
- # path_type('doc/api') # => 'tree'
- #
- # Returns a String
- def path_type(path)
- unescaped_path = Addressable::URI.unescape(path)
-
- if tree?(unescaped_path)
- 'tree'
- elsif image?(unescaped_path)
- 'raw'
- else
- 'blob'
- end
+ path.present? && !!uri_type(path)
end
- def tree?(path)
- repository.tree(current_sha, path).entries.any?
- end
+ def uri_type(path)
+ @uri_types[path] ||= begin
+ unescaped_path = Addressable::URI.unescape(path)
- def image?(path)
- repository.blob_at(current_sha, path).try(:image?)
+ current_commit.uri_type(unescaped_path)
+ end
end
- def current_sha
- context[:commit].try(:id) ||
- ref ? repository.commit(ref).try(:sha) : repository.head_commit.sha
+ def current_commit
+ @current_commit ||= context[:commit] ||
+ ref ? repository.commit(ref) : repository.head_commit
end
def relative_url_root
@@ -148,7 +125,7 @@ module Banzai
end
def repository
- context[:project].try(:repository)
+ @repository ||= context[:project].try(:repository)
end
end
end
diff --git a/lib/banzai/filter/wiki_link_filter.rb b/lib/banzai/filter/wiki_link_filter.rb
index 37a2779d453..1bb6d6bba87 100644
--- a/lib/banzai/filter/wiki_link_filter.rb
+++ b/lib/banzai/filter/wiki_link_filter.rb
@@ -29,7 +29,7 @@ module Banzai
return if html_attr.blank?
html_attr.value = apply_rewrite_rules(html_attr.value)
- rescue URI::Error
+ rescue URI::Error, Addressable::URI::InvalidURIError
# noop
end
diff --git a/lib/banzai/renderer.rb b/lib/banzai/renderer.rb
index c14a9c4c722..6718acdef7e 100644
--- a/lib/banzai/renderer.rb
+++ b/lib/banzai/renderer.rb
@@ -30,13 +30,9 @@ module Banzai
end
def self.render_result(text, context = {})
- Pipeline[context[:pipeline]].call(text, context)
- end
+ text = Pipeline[:pre_process].to_html(text, context) if text
- def self.pre_process(text, context)
- pipeline = Pipeline[:pre_process]
-
- pipeline.to_html(text, context)
+ Pipeline[context[:pipeline]].call(text, context)
end
# Perform post-processing on an HTML String
diff --git a/lib/ci/api/runners.rb b/lib/ci/api/runners.rb
index 0c41f22c7c5..bcc82969eb3 100644
--- a/lib/ci/api/runners.rb
+++ b/lib/ci/api/runners.rb
@@ -28,12 +28,9 @@ module Ci
post "register" do
required_attributes! [:token]
- attributes = { description: params[:description],
- tag_list: params[:tag_list] }
-
- unless params[:run_untagged].nil?
- attributes[:run_untagged] = params[:run_untagged]
- end
+ attributes = attributes_for_keys(
+ [:description, :tag_list, :run_untagged, :locked]
+ )
runner =
if runner_registration_token_valid?
diff --git a/lib/container_registry/tag.rb b/lib/container_registry/tag.rb
index 7a0929d774e..708d01b95a1 100644
--- a/lib/container_registry/tag.rb
+++ b/lib/container_registry/tag.rb
@@ -3,6 +3,7 @@ module ContainerRegistry
attr_reader :repository, :name
delegate :registry, :client, to: :repository
+ delegate :revision, :short_revision, to: :config_blob, allow_nil: true
def initialize(repository, name)
@repository, @name = repository, name
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 6d0e30e916f..831f1e635ba 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -5,6 +5,8 @@
#
module Gitlab
module Access
+ class AccessDeniedError < StandardError; end
+
GUEST = 10
REPORTER = 20
DEVELOPER = 30
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 7e3f5abba62..ab7b811c5d8 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -31,7 +31,7 @@ module Grack
auth!
- lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
+ lfs_response = Gitlab::Lfs::Router.new(project, @user, @ci, @request).try_call
return lfs_response unless lfs_response.nil?
if @user.nil? && !@ci
diff --git a/lib/gitlab/database.rb b/lib/gitlab/database.rb
index d76ecb54017..078609c86f1 100644
--- a/lib/gitlab/database.rb
+++ b/lib/gitlab/database.rb
@@ -1,5 +1,10 @@
module Gitlab
module Database
+ # The max value of INTEGER type is the same between MySQL and PostgreSQL:
+ # https://www.postgresql.org/docs/9.2/static/datatype-numeric.html
+ # http://dev.mysql.com/doc/refman/5.7/en/integer-types.html
+ MAX_INT_VALUE = 2147483647
+
def self.adapter_name
connection.adapter_name
end
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index e2fee6b9f3e..047c77c6fc2 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -37,7 +37,7 @@ module Gitlab
end
def diffs
- @diffs ||= (safe_diff_files(compare.diffs, diff_refs) if compare)
+ @diffs ||= (safe_diff_files(compare.diffs(max_files: 30), diff_refs) if compare)
end
def diffs_count
diff --git a/lib/gitlab/github_import/importer.rb b/lib/gitlab/github_import/importer.rb
index e5cf66a0371..2286ac8829c 100644
--- a/lib/gitlab/github_import/importer.rb
+++ b/lib/gitlab/github_import/importer.rb
@@ -66,8 +66,7 @@ module Gitlab
end
def import_pull_requests
- hooks = client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
- disable_webhooks(hooks)
+ disable_webhooks
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?)
@@ -90,14 +89,14 @@ module Gitlab
raise Projects::ImportService::Error, e.message
ensure
clean_up_restored_branches(branches_removed)
- clean_up_disabled_webhooks(hooks)
+ clean_up_disabled_webhooks
end
- def disable_webhooks(hooks)
+ def disable_webhooks
update_webhooks(hooks, active: false)
end
- def clean_up_disabled_webhooks(hooks)
+ def clean_up_disabled_webhooks
update_webhooks(hooks, active: true)
end
@@ -107,6 +106,20 @@ module Gitlab
end
end
+ def hooks
+ @hooks ||=
+ begin
+ client.hooks(repo).map { |raw| HookFormatter.new(raw) }.select(&:valid?)
+
+ # The GitHub Repository Webhooks API returns 404 for users
+ # without admin access to the repository when listing hooks.
+ # In this case we just want to return gracefully instead of
+ # spitting out an error and stop the import process.
+ rescue Octokit::NotFound
+ []
+ end
+ end
+
def restore_branches(branches)
branches.each do |name, sha|
client.create_ref(repo, "refs/heads/#{name}", sha)
diff --git a/lib/gitlab/gitignore.rb b/lib/gitlab/gitignore.rb
deleted file mode 100644
index f46b43b61a4..00000000000
--- a/lib/gitlab/gitignore.rb
+++ /dev/null
@@ -1,56 +0,0 @@
-module Gitlab
- class Gitignore
- FILTER_REGEX = /\.gitignore\z/.freeze
-
- def initialize(path)
- @path = path
- end
-
- def name
- File.basename(@path, '.gitignore')
- end
-
- def content
- File.read(@path)
- end
-
- class << self
- def all
- languages_frameworks + global
- end
-
- def find(key)
- file_name = "#{key}.gitignore"
-
- directory = select_directory(file_name)
- directory ? new(File.join(directory, file_name)) : nil
- end
-
- def global
- files_for_folder(global_dir).map { |file| new(File.join(global_dir, file)) }
- end
-
- def languages_frameworks
- files_for_folder(gitignore_dir).map { |file| new(File.join(gitignore_dir, file)) }
- end
-
- private
-
- def select_directory(file_name)
- [gitignore_dir, global_dir].find { |dir| File.exist?(File.join(dir, file_name)) }
- end
-
- def global_dir
- File.join(gitignore_dir, 'Global')
- end
-
- def gitignore_dir
- Rails.root.join('vendor/gitignore')
- end
-
- def files_for_folder(dir)
- Dir.glob("#{dir.to_s}/*.gitignore").map { |file| file.gsub(FILTER_REGEX, '') }
- end
- end
- end
-end
diff --git a/lib/gitlab/import_export.rb b/lib/gitlab/import_export.rb
index 624c1766024..99cf85d9a3b 100644
--- a/lib/gitlab/import_export.rb
+++ b/lib/gitlab/import_export.rb
@@ -21,7 +21,7 @@ module Gitlab
end
def config_file
- 'lib/gitlab/import_export/import_export.yml'
+ Rails.root.join('lib/gitlab/import_export/import_export.yml')
end
def version_filename
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
index 9d9617761b3..e3ed2f6791d 100644
--- a/lib/gitlab/lfs/response.rb
+++ b/lib/gitlab/lfs/response.rb
@@ -2,10 +2,11 @@ module Gitlab
module Lfs
class Response
- def initialize(project, user, request)
+ def initialize(project, user, ci, request)
@origin_project = project
@project = storage_project(project)
@user = user
+ @ci = ci
@env = request.env
@request = request
end
@@ -189,7 +190,7 @@ module Gitlab
return render_not_enabled unless Gitlab.config.lfs.enabled
unless @project.public?
- return render_unauthorized unless @user
+ return render_unauthorized unless @user || @ci
return render_forbidden unless user_can_fetch?
end
@@ -210,7 +211,7 @@ module Gitlab
def user_can_fetch?
# Check user access against the project they used to initiate the pull
- @user.can?(:download_code, @origin_project)
+ @ci || @user.can?(:download_code, @origin_project)
end
def user_can_push?
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
index 78d02891102..69bd5e62305 100644
--- a/lib/gitlab/lfs/router.rb
+++ b/lib/gitlab/lfs/router.rb
@@ -1,9 +1,12 @@
module Gitlab
module Lfs
class Router
- def initialize(project, user, request)
+ attr_reader :project, :user, :ci, :request
+
+ def initialize(project, user, ci, request)
@project = project
@user = user
+ @ci = ci
@env = request.env
@request = request
end
@@ -80,7 +83,7 @@ module Gitlab
def lfs
return unless @project
- Gitlab::Lfs::Response.new(@project, @user, @request)
+ Gitlab::Lfs::Response.new(@project, @user, @ci, @request)
end
def sanitize_tmp_filename(name)
diff --git a/lib/gitlab/template/base_template.rb b/lib/gitlab/template/base_template.rb
new file mode 100644
index 00000000000..760ff3e614a
--- /dev/null
+++ b/lib/gitlab/template/base_template.rb
@@ -0,0 +1,67 @@
+module Gitlab
+ module Template
+ class BaseTemplate
+ def initialize(path)
+ @path = path
+ end
+
+ def name
+ File.basename(@path, self.class.extension)
+ end
+
+ def content
+ File.read(@path)
+ end
+
+ class << self
+ def all
+ self.categories.keys.flat_map { |cat| by_category(cat) }
+ end
+
+ def find(key)
+ file_name = "#{key}#{self.extension}"
+
+ directory = select_directory(file_name)
+ directory ? new(File.join(category_directory(directory), file_name)) : nil
+ end
+
+ def categories
+ raise NotImplementedError
+ end
+
+ def extension
+ raise NotImplementedError
+ end
+
+ def base_dir
+ raise NotImplementedError
+ end
+
+ def by_category(category)
+ templates_for_directory(category_directory(category))
+ end
+
+ def category_directory(category)
+ File.join(base_dir, categories[category])
+ end
+
+ private
+
+ def select_directory(file_name)
+ categories.keys.find do |category|
+ File.exist?(File.join(category_directory(category), file_name))
+ end
+ end
+
+ def templates_for_directory(dir)
+ dir << '/' unless dir.end_with?('/')
+ Dir.glob(File.join(dir, "*#{self.extension}")).select { |f| f =~ filter_regex }.map { |f| new(f) }
+ end
+
+ def filter_regex
+ @filter_reges ||= /#{Regexp.escape(extension)}\z/
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitignore.rb b/lib/gitlab/template/gitignore.rb
new file mode 100644
index 00000000000..964fbfd4de3
--- /dev/null
+++ b/lib/gitlab/template/gitignore.rb
@@ -0,0 +1,22 @@
+module Gitlab
+ module Template
+ class Gitignore < BaseTemplate
+ class << self
+ def extension
+ '.gitignore'
+ end
+
+ def categories
+ {
+ "Languages" => '',
+ "Global" => 'Global'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitignore')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/template/gitlab_ci_yml.rb b/lib/gitlab/template/gitlab_ci_yml.rb
new file mode 100644
index 00000000000..7f480fe33c0
--- /dev/null
+++ b/lib/gitlab/template/gitlab_ci_yml.rb
@@ -0,0 +1,27 @@
+module Gitlab
+ module Template
+ class GitlabCiYml < BaseTemplate
+ def content
+ explanation = "# This file is a template, and might need editing before it works on your project."
+ [explanation, super].join("\n")
+ end
+
+ class << self
+ def extension
+ '.gitlab-ci.yml'
+ end
+
+ def categories
+ {
+ "General" => '',
+ "Pages" => 'Pages'
+ }
+ end
+
+ def base_dir
+ Rails.root.join('vendor/gitlab-ci-yml')
+ end
+ end
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/import_export.rake b/lib/tasks/gitlab/import_export.rake
new file mode 100644
index 00000000000..c2c6031db67
--- /dev/null
+++ b/lib/tasks/gitlab/import_export.rake
@@ -0,0 +1,13 @@
+namespace :gitlab do
+ namespace :import_export do
+ desc "GitLab | Show Import/Export version"
+ task version: :environment do
+ puts "Import/Export v#{Gitlab::ImportExport.version}"
+ end
+
+ desc "GitLab | Display exported DB structure"
+ task data: :environment do
+ puts YAML.load_file(Gitlab::ImportExport.config_file)['project_tree'].to_yaml(:SortKeys => true)
+ end
+ end
+end
diff --git a/lib/tasks/gitlab/update_gitignore.rake b/lib/tasks/gitlab/update_gitignore.rake
deleted file mode 100644
index 4fd48cccb1d..00000000000
--- a/lib/tasks/gitlab/update_gitignore.rake
+++ /dev/null
@@ -1,46 +0,0 @@
-namespace :gitlab do
- desc "GitLab | Update gitignore"
- task :update_gitignore do
- unless clone_gitignores
- puts "Cloning the gitignores failed".color(:red)
- return
- end
-
- remove_unneeded_files(gitignore_directory)
- remove_unneeded_files(global_directory)
-
- puts "Done".color(:green)
- end
-
- def clone_gitignores
- FileUtils.rm_rf(gitignore_directory) if Dir.exist?(gitignore_directory)
- FileUtils.cd vendor_directory
-
- system('git clone --depth=1 --branch=master https://github.com/github/gitignore.git')
- end
-
- # Retain only certain files:
- # - The LICENSE, because we have to
- # - The sub dir global
- # - The gitignores themself
- # - Dir.entires returns also the entries '.' and '..'
- def remove_unneeded_files(path)
- Dir.foreach(path) do |file|
- FileUtils.rm_rf(File.join(path, file)) unless file =~ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/
- end
- end
-
- private
-
- def vendor_directory
- Rails.root.join('vendor')
- end
-
- def gitignore_directory
- File.join(vendor_directory, 'gitignore')
- end
-
- def global_directory
- File.join(gitignore_directory, 'Global')
- end
-end
diff --git a/lib/tasks/gitlab/update_templates.rake b/lib/tasks/gitlab/update_templates.rake
new file mode 100644
index 00000000000..4f76dad7286
--- /dev/null
+++ b/lib/tasks/gitlab/update_templates.rake
@@ -0,0 +1,54 @@
+namespace :gitlab do
+ desc "GitLab | Update templates"
+ task :update_templates do
+ TEMPLATE_DATA.each { |template| update(template) }
+ end
+
+ def update(template)
+ sub_dir = template.repo_url.match(/([a-z-]+)\.git\z/)[1]
+ dir = File.join(vendor_directory, sub_dir)
+
+ unless clone_repository(template.repo_url, dir)
+ puts "Cloning the #{sub_dir} templates failed".red
+ return
+ end
+
+ remove_unneeded_files(dir, template.cleanup_regex)
+ puts "Done".green
+ end
+
+ def clone_repository(url, directory)
+ FileUtils.rm_rf(directory) if Dir.exist?(directory)
+
+ system("git clone #{url} --depth=1 --branch=master #{directory}")
+ end
+
+ # Retain only certain files:
+ # - The LICENSE, because we have to
+ # - The sub dirs so we can organise the file by category
+ # - The templates themself
+ # - Dir.entries returns also the entries '.' and '..'
+ def remove_unneeded_files(directory, regex)
+ Dir.foreach(directory) do |file|
+ FileUtils.rm_rf(File.join(directory, file)) unless file =~ regex
+ end
+ end
+
+ private
+
+ Template = Struct.new(:repo_url, :cleanup_regex)
+ TEMPLATE_DATA = [
+ Template.new(
+ "https://github.com/github/gitignore.git",
+ /(\.{1,2}|LICENSE|Global|\.gitignore)\z/
+ ),
+ Template.new(
+ "https://gitlab.com/gitlab-org/gitlab-ci-yml.git",
+ /(\.{1,2}|LICENSE|Pages|\.gitlab-ci.yml)\z/
+ )
+ ]
+
+ def vendor_directory
+ Rails.root.join('vendor')
+ end
+end
diff --git a/spec/controllers/groups/group_members_controller_spec.rb b/spec/controllers/groups/group_members_controller_spec.rb
index 89c2c26a367..c8601341d54 100644
--- a/spec/controllers/groups/group_members_controller_spec.rb
+++ b/spec/controllers/groups/group_members_controller_spec.rb
@@ -118,9 +118,7 @@ describe Groups::GroupMembersController do
it 'cannot removes himself from the group' do
delete :leave, group_id: group
- expect(response).to redirect_to(group_path(group))
- expect(response).to set_flash[:alert].to "You can not leave the \"#{group.name}\" group. Transfer or delete the group."
- expect(group.users).to include user
+ expect(response.status).to eq(403)
end
end
@@ -134,7 +132,7 @@ describe Groups::GroupMembersController do
delete :leave, group_id: group
expect(response).to set_flash.to 'Your access request to the group has been withdrawn.'
- expect(response).to redirect_to(dashboard_groups_path)
+ expect(response).to redirect_to(group_path(group))
expect(group.members.request).to be_empty
expect(group.users).not_to include user
end
diff --git a/spec/controllers/groups/notification_settings_controller_spec.rb b/spec/controllers/groups/notification_settings_controller_spec.rb
deleted file mode 100644
index 0786e45515a..00000000000
--- a/spec/controllers/groups/notification_settings_controller_spec.rb
+++ /dev/null
@@ -1,32 +0,0 @@
-require 'spec_helper'
-
-describe Groups::NotificationSettingsController do
- let(:group) { create(:group) }
- let(:user) { create(:user) }
-
- describe '#update' do
- context 'when not authorized' do
- it 'redirects to sign in page' do
- put :update,
- group_id: group.to_param,
- notification_setting: { level: :participating }
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'when authorized' do
- before do
- sign_in(user)
- end
-
- it 'returns success' do
- put :update,
- group_id: group.to_param,
- notification_setting: { level: :participating }
-
- expect(response.status).to eq 200
- end
- end
- end
-end
diff --git a/spec/controllers/notification_settings_controller_spec.rb b/spec/controllers/notification_settings_controller_spec.rb
new file mode 100644
index 00000000000..15d155833b4
--- /dev/null
+++ b/spec/controllers/notification_settings_controller_spec.rb
@@ -0,0 +1,125 @@
+require 'spec_helper'
+
+describe NotificationSettingsController do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ describe '#create' do
+ context 'when not authorized' do
+ it 'redirects to sign in page' do
+ post :create,
+ project: { id: project.id },
+ notification_setting: { level: :participating }
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ end
+
+ it 'returns success' do
+ post :create,
+ project: { id: project.id },
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq 200
+ end
+
+ context 'and setting custom notification setting' do
+ let(:custom_events) do
+ events = {}
+
+ NotificationSetting::EMAIL_EVENTS.each do |event|
+ events[event] = "true"
+ end
+ end
+
+ it 'returns success' do
+ post :create,
+ project: { id: project.id },
+ notification_setting: { level: :participating, events: custom_events }
+
+ expect(response.status).to eq 200
+ end
+ end
+ end
+
+ context 'not authorized' do
+ let(:private_project) { create(:project, :private) }
+ before { sign_in(user) }
+
+ it 'returns 404' do
+ post :create,
+ project: { id: private_project.id },
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+
+ describe '#update' do
+ let(:notification_setting) { user.global_notification_setting }
+
+ context 'when not authorized' do
+ it 'redirects to sign in page' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating }
+
+ expect(response).to redirect_to(new_user_session_path)
+ end
+ end
+
+ context 'when authorized' do
+ before{ sign_in(user) }
+
+ it 'returns success' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq 200
+ end
+
+ context 'and setting custom notification setting' do
+ let(:custom_events) do
+ events = {}
+
+ NotificationSetting::EMAIL_EVENTS.each do |event|
+ events[event] = "true"
+ end
+ end
+
+ it 'returns success' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating, events: custom_events }
+
+ expect(response.status).to eq 200
+ end
+ end
+ end
+
+ context 'not authorized' do
+ let(:other_user) { create(:user) }
+
+ before { sign_in(other_user) }
+
+ it 'returns 404' do
+ put :update,
+ id: notification_setting,
+ notification_setting: { level: :participating }
+
+ expect(response.status).to eq(404)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/notification_settings_controller_spec.rb b/spec/controllers/projects/notification_settings_controller_spec.rb
deleted file mode 100644
index c5d17d97ec9..00000000000
--- a/spec/controllers/projects/notification_settings_controller_spec.rb
+++ /dev/null
@@ -1,52 +0,0 @@
-require 'spec_helper'
-
-describe Projects::NotificationSettingsController do
- let(:project) { create(:empty_project) }
- let(:user) { create(:user) }
-
- before do
- project.team << [user, :developer]
- end
-
- describe '#update' do
- context 'when not authorized' do
- it 'redirects to sign in page' do
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- notification_setting: { level: :participating }
-
- expect(response).to redirect_to(new_user_session_path)
- end
- end
-
- context 'when authorized' do
- before do
- sign_in(user)
- end
-
- it 'returns success' do
- put :update,
- namespace_id: project.namespace.to_param,
- project_id: project.to_param,
- notification_setting: { level: :participating }
-
- expect(response.status).to eq 200
- end
- end
-
- context 'not authorized' do
- let(:private_project) { create(:project, :private) }
- before { sign_in(user) }
-
- it 'returns 404' do
- put :update,
- namespace_id: private_project.namespace.to_param,
- project_id: private_project.to_param,
- notification_setting: { level: :participating }
-
- expect(response.status).to eq(404)
- end
- end
- end
-end
diff --git a/spec/controllers/projects/project_members_controller_spec.rb b/spec/controllers/projects/project_members_controller_spec.rb
index fc5f458e795..e5e750c855f 100644
--- a/spec/controllers/projects/project_members_controller_spec.rb
+++ b/spec/controllers/projects/project_members_controller_spec.rb
@@ -171,11 +171,7 @@ describe Projects::ProjectMembersController do
delete :leave, namespace_id: project.namespace,
project_id: project
- expect(response).to redirect_to(
- namespace_project_path(project.namespace, project)
- )
- expect(response).to set_flash[:alert].to "You can not leave the \"#{project.human_name}\" project. Transfer or delete the project."
- expect(project.users).to include user
+ expect(response.status).to eq(403)
end
end
@@ -190,7 +186,7 @@ describe Projects::ProjectMembersController do
project_id: project
expect(response).to set_flash.to 'Your access request to the project has been withdrawn.'
- expect(response).to redirect_to(dashboard_projects_path)
+ expect(response).to redirect_to(namespace_project_path(project.namespace, project))
expect(project.members.request).to be_empty
expect(project.users).not_to include user
end
diff --git a/spec/controllers/projects/todo_controller_spec.rb b/spec/controllers/projects/todo_controller_spec.rb
new file mode 100644
index 00000000000..40a3403b660
--- /dev/null
+++ b/spec/controllers/projects/todo_controller_spec.rb
@@ -0,0 +1,102 @@
+require('spec_helper')
+
+describe Projects::TodosController do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let(:issue) { create(:issue, project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+
+ context 'Issues' do
+ describe 'POST create' do
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'should create todo for issue' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(1)
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not authorized' do
+ it 'should not create todo for issue that user has no access to' do
+ sign_in(user)
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'should not create todo for issue when user not logged in' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: issue.id,
+ issuable_type: 'issue')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(302)
+ end
+ end
+ end
+ end
+
+ context 'Merge Requests' do
+ describe 'POST create' do
+ context 'when authorized' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it 'should create todo for merge request' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(1)
+
+ expect(response.status).to eq(200)
+ end
+ end
+
+ context 'when not authorized' do
+ it 'should not create todo for merge request user has no access to' do
+ sign_in(user)
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(404)
+ end
+
+ it 'should not create todo for merge request user has no access to' do
+ expect do
+ post(:create, namespace_id: project.namespace.path,
+ project_id: project.path,
+ issuable_id: merge_request.id,
+ issuable_type: 'merge_request')
+ end.to change { user.todos.count }.by(0)
+
+ expect(response.status).to eq(302)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb
index fba545560c7..146b2c2e131 100644
--- a/spec/controllers/projects_controller_spec.rb
+++ b/spec/controllers/projects_controller_spec.rb
@@ -237,4 +237,24 @@ describe ProjectsController do
expect(response.status).to eq(401)
end
end
+
+ describe "GET refs" do
+ it "should get a list of branches and tags" do
+ get :refs, namespace_id: public_project.namespace.path, id: public_project.path
+
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body["Branches"]).to include("master")
+ expect(parsed_body["Tags"]).to include("v1.0.0")
+ expect(parsed_body["Commits"]).to be_nil
+ end
+
+ it "should get a list of branches, tags and commits" do
+ get :refs, namespace_id: public_project.namespace.path, id: public_project.path, ref: "123456"
+
+ parsed_body = JSON.parse(response.body)
+ expect(parsed_body["Branches"]).to include("master")
+ expect(parsed_body["Tags"]).to include("v1.0.0")
+ expect(parsed_body["Commits"]).to include("123456")
+ end
+ end
end
diff --git a/spec/factories/ci/commits.rb b/spec/factories/ci/pipelines.rb
index a039bef6f3c..a039bef6f3c 100644
--- a/spec/factories/ci/commits.rb
+++ b/spec/factories/ci/pipelines.rb
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index 9499cd4e025..2d297776cb0 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -60,6 +60,40 @@ describe "Admin Runners" do
it { expect(page).to have_content(@project1.name_with_namespace) }
it { expect(page).not_to have_content(@project2.name_with_namespace) }
end
+
+ describe 'enable/create' do
+ before do
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it 'enables specific runner for project' do
+ within '.unassigned-projects' do
+ click_on 'Enable'
+ end
+
+ assigned_project = page.find('.assigned-projects')
+
+ expect(assigned_project).to have_content(@project2.path)
+ end
+ end
+
+ describe 'disable/destroy' do
+ before do
+ @project1.runners << runner
+ visit admin_runner_path(runner)
+ end
+
+ it 'enables specific runner for project' do
+ within '.assigned-projects' do
+ click_on 'Disable'
+ end
+
+ new_runner_project = page.find('.unassigned-projects')
+
+ expect(new_runner_project).to have_content(@project1.path)
+ end
+ end
end
describe 'runners registration token' do
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index 53b4f027117..203e55a36f2 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -26,7 +26,8 @@ describe "Container Registry" do
end
context 'when there are tags' do
- it { expect(page).to have_content(tag_name)}
+ it { expect(page).to have_content(tag_name) }
+ it { expect(page).to have_content('d7a513a66') }
end
end
diff --git a/spec/features/environments_spec.rb b/spec/features/environments_spec.rb
index 40fea5211e9..7fb28f4174b 100644
--- a/spec/features/environments_spec.rb
+++ b/spec/features/environments_spec.rb
@@ -20,7 +20,7 @@ feature 'Environments', feature: true do
context 'without environments' do
scenario 'does show no environments' do
- expect(page).to have_content('No environments to show')
+ expect(page).to have_content('You don\'t have any environments right now.')
end
end
@@ -61,7 +61,7 @@ feature 'Environments', feature: true do
context 'without deployments' do
scenario 'does show no deployments' do
- expect(page).to have_content('No deployments for')
+ expect(page).to have_content('You don\'t have any deployments right now.')
end
end
@@ -108,7 +108,7 @@ feature 'Environments', feature: true do
end
scenario 'does create a new pipeline' do
- expect(page).to have_content('production')
+ expect(page).to have_content('Production')
end
end
diff --git a/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
new file mode 100644
index 00000000000..33bf6d3752f
--- /dev/null
+++ b/spec/features/groups/members/last_owner_cannot_leave_group_spec.rb
@@ -0,0 +1,16 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Last owner cannot leave group', feature: true do
+ let(:owner) { create(:user) }
+ let(:group) { create(:group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(owner)
+ visit group_path(group)
+ end
+
+ scenario 'user does not see a "Leave Group" link' do
+ expect(page).not_to have_content 'Leave Group'
+ end
+end
diff --git a/spec/features/groups/members/member_leaves_group_spec.rb b/spec/features/groups/members/member_leaves_group_spec.rb
new file mode 100644
index 00000000000..3185ff924b9
--- /dev/null
+++ b/spec/features/groups/members/member_leaves_group_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Groups > Members > Member leaves group', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ background do
+ group.add_owner(owner)
+ group.add_developer(user)
+ login_as(user)
+ visit group_path(group)
+ end
+
+ scenario 'user leaves group' do
+ click_link 'Leave Group'
+
+ expect(current_path).to eq(dashboard_groups_path)
+ expect(group.users.exists?(user.id)).to be_falsey
+ end
+end
diff --git a/spec/features/groups/members/owner_manages_access_requests_spec.rb b/spec/features/groups/members/owner_manages_access_requests_spec.rb
index 22525ce530b..321c9bad7d0 100644
--- a/spec/features/groups/members/owner_manages_access_requests_spec.rb
+++ b/spec/features/groups/members/owner_manages_access_requests_spec.rb
@@ -42,7 +42,7 @@ feature 'Groups > Members > Owner manages access requests', feature: true do
def expect_visible_access_request(group, user)
expect(group.members.request.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "#{group.name} access requests (1)"
+ expect(page).to have_content "#{group.name} access requests 1"
expect(page).to have_content user.name
end
end
diff --git a/spec/features/groups/members/user_requests_access_spec.rb b/spec/features/groups/members/user_requests_access_spec.rb
index a878a96b6ee..1ea607cbca0 100644
--- a/spec/features/groups/members/user_requests_access_spec.rb
+++ b/spec/features/groups/members/user_requests_access_spec.rb
@@ -21,6 +21,7 @@ feature 'Groups > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
+ expect(page).not_to have_content 'Leave Group'
end
scenario 'user is not listed in the group members page' do
diff --git a/spec/features/issues/bulk_assigment_labels_spec.rb b/spec/features/issues/bulk_assignment_labels_spec.rb
index 0fbc2062e39..7143d0e40f3 100644
--- a/spec/features/issues/bulk_assigment_labels_spec.rb
+++ b/spec/features/issues/bulk_assignment_labels_spec.rb
@@ -190,7 +190,8 @@ feature 'Issues > Labels bulk assignment', feature: true do
end
if unmark
items.map do |item|
- click_link item
+ # Make sure we are unmarking the item no matter the state it has currently
+ click_link item until find('a', text: item)[:class] == 'label-item'
end
end
end
diff --git a/spec/features/issues/todo_spec.rb b/spec/features/issues/todo_spec.rb
index b69cce3e7d7..bc0f437a8ce 100644
--- a/spec/features/issues/todo_spec.rb
+++ b/spec/features/issues/todo_spec.rb
@@ -20,6 +20,12 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
page.within '.header-content .todos-pending-count' do
expect(page).to have_content '1'
end
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ page.within '.header-content .todos-pending-count' do
+ expect(page).to have_content '1'
+ end
end
it 'should mark a todo as done' do
@@ -29,5 +35,9 @@ feature 'Manually create a todo item from issue', feature: true, js: true do
end
expect(page).to have_selector('.todos-pending-count', visible: false)
+
+ visit namespace_project_issue_path(project.namespace, project, issue)
+
+ expect(page).to have_selector('.todos-pending-count', visible: false)
end
end
diff --git a/spec/features/issues_spec.rb b/spec/features/issues_spec.rb
index c3cb3379440..5065dfb849c 100644
--- a/spec/features/issues_spec.rb
+++ b/spec/features/issues_spec.rb
@@ -22,7 +22,7 @@ describe 'Issues', feature: true do
before do
visit edit_namespace_project_issue_path(project.namespace, project, issue)
- click_button "Go full screen"
+ find('.js-zen-enter').click
end
it 'should open new issue popup' do
diff --git a/spec/features/projects/badges/list_spec.rb b/spec/features/projects/badges/list_spec.rb
index 51be81d634c..01e90618a98 100644
--- a/spec/features/projects/badges/list_spec.rb
+++ b/spec/features/projects/badges/list_spec.rb
@@ -1,8 +1,6 @@
require 'spec_helper'
feature 'list of badges' do
- include Select2Helper
-
background do
user = create(:user)
project = create(:project)
@@ -24,7 +22,11 @@ feature 'list of badges' do
end
scenario 'user changes current ref on badges list page', js: true do
- select2('improve/awesome', from: '#ref')
+ first('.js-project-refs-dropdown').click
+
+ page.within '.project-refs-form' do
+ click_link 'improve/awesome'
+ end
expect(page).to have_content 'badges/improve/awesome/build.svg'
end
diff --git a/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
new file mode 100644
index 00000000000..d516e8ce55a
--- /dev/null
+++ b/spec/features/projects/files/gitlab_ci_yml_dropdown_spec.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+feature 'User wants to add a .gitlab-ci.yml file', feature: true do
+ include WaitForAjax
+
+ 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: '.gitlab-ci.yml')
+ end
+
+ scenario 'user can see .gitlab-ci.yml dropdown' do
+ expect(page).to have_css('.gitlab-ci-yml-selector')
+ end
+
+ scenario 'user can pick a template from the dropdown', js: true do
+ find('.js-gitlab-ci-yml-selector').click
+ wait_for_ajax
+ within '.gitlab-ci-yml-selector' do
+ find('.dropdown-input-field').set('jekyll')
+ find('.dropdown-content li', text: 'jekyll').click
+ end
+ wait_for_ajax
+
+ expect(page).to have_content('This file is a template, and might need editing before it works on your project')
+ expect(page).to have_content('jekyll build -d test')
+ end
+end
diff --git a/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
new file mode 100644
index 00000000000..4d5d656f00c
--- /dev/null
+++ b/spec/features/projects/members/group_member_cannot_request_access_to_his_group_project_spec.rb
@@ -0,0 +1,50 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group member cannot request access to his group project', feature: true do
+ let(:user) { create(:user) }
+ let(:group) { create(:group) }
+ let(:project) { create(:project, namespace: group) }
+
+ background do
+ end
+
+ scenario 'owner does not see the request access button' do
+ group.add_owner(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'master does not see the request access button' do
+ group.add_master(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'developer does not see the request access button' do
+ group.add_developer(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'reporter does not see the request access button' do
+ group.add_reporter(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ scenario 'guest does not see the request access button' do
+ group.add_guest(user)
+ login_and_visit_project_page(user)
+
+ expect(page).not_to have_content 'Request Access'
+ end
+
+ def login_and_visit_project_page(user)
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+end
diff --git a/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
new file mode 100644
index 00000000000..c4ed92d2780
--- /dev/null
+++ b/spec/features/projects/members/group_requester_cannot_request_access_to_project_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Group requester cannot request access to project', feature: true do
+ let(:user) { create(:user) }
+ let(:owner) { create(:user) }
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:project, :public, namespace: group) }
+
+ background do
+ group.add_owner(owner)
+ login_as(user)
+ visit group_path(group)
+ perform_enqueued_jobs { click_link 'Request Access' }
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'group requester does not see the request access / withdraw access request button' do
+ expect(page).not_to have_content 'Request Access'
+ expect(page).not_to have_content 'Withdraw Access Request'
+ end
+end
diff --git a/spec/features/projects/members/master_manages_access_requests_spec.rb b/spec/features/projects/members/master_manages_access_requests_spec.rb
index 5fe4caa12f0..aa2d906fa2e 100644
--- a/spec/features/projects/members/master_manages_access_requests_spec.rb
+++ b/spec/features/projects/members/master_manages_access_requests_spec.rb
@@ -41,7 +41,7 @@ feature 'Projects > Members > Master manages access requests', feature: true do
def expect_visible_access_request(project, user)
expect(project.members.request.exists?(user_id: user)).to be_truthy
- expect(page).to have_content "#{project.name} access requests (1)"
+ expect(page).to have_content "#{project.name} access requests 1"
expect(page).to have_content user.name
end
end
diff --git a/spec/features/projects/members/member_leaves_project_spec.rb b/spec/features/projects/members/member_leaves_project_spec.rb
new file mode 100644
index 00000000000..79dec442818
--- /dev/null
+++ b/spec/features/projects/members/member_leaves_project_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+feature 'Projects > Members > Member leaves project', feature: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+
+ background do
+ project.team << [user, :developer]
+ login_as(user)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user leaves project' do
+ click_link 'Leave Project'
+
+ expect(current_path).to eq(dashboard_projects_path)
+ expect(project.users.exists?(user.id)).to be_falsey
+ end
+end
diff --git a/spec/features/projects/members/owner_cannot_leave_project_spec.rb b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
new file mode 100644
index 00000000000..67811b1048e
--- /dev/null
+++ b/spec/features/projects/members/owner_cannot_leave_project_spec.rb
@@ -0,0 +1,16 @@
+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)
+ visit namespace_project_path(project.namespace, project)
+ end
+
+ scenario 'user does not see a "Leave Project" link' do
+ expect(page).not_to have_content 'Leave Project'
+ end
+end
diff --git a/spec/features/projects/members/user_requests_access_spec.rb b/spec/features/projects/members/user_requests_access_spec.rb
index fd92a3a2f0c..af420c170ef 100644
--- a/spec/features/projects/members/user_requests_access_spec.rb
+++ b/spec/features/projects/members/user_requests_access_spec.rb
@@ -21,6 +21,7 @@ feature 'Projects > Members > User requests access', feature: true do
expect(page).to have_content 'Your request for access has been queued for review.'
expect(page).to have_content 'Withdraw Access Request'
+ expect(page).not_to have_content 'Leave Project'
end
scenario 'user is not listed in the project members page' do
diff --git a/spec/features/projects_spec.rb b/spec/features/projects_spec.rb
index 9dd0378d165..6fa8298d489 100644
--- a/spec/features/projects_spec.rb
+++ b/spec/features/projects_spec.rb
@@ -70,22 +70,6 @@ feature 'Project', feature: true do
end
end
- describe 'leave project link' do
- let(:user) { create(:user) }
- let(:project) { create(:project, namespace: user.namespace) }
-
- before do
- login_with(user)
- project.team.add_user(user, Gitlab::Access::MASTER)
- visit namespace_project_path(project.namespace, project)
- end
-
- it 'click project-settings and find leave project' do
- find('#project-settings-button').click
- expect(page).to have_link('Leave Project')
- end
- end
-
describe 'project title' do
include WaitForAjax
diff --git a/spec/fixtures/container_registry/tag_manifest.json b/spec/fixtures/container_registry/tag_manifest.json
index 1b6008e2872..8d1b874c29b 100644
--- a/spec/fixtures/container_registry/tag_manifest.json
+++ b/spec/fixtures/container_registry/tag_manifest.json
@@ -1 +1,16 @@
-{"schemaVersion":2,"mediaType":"application/vnd.docker.distribution.manifest.v2+json","config":{"mediaType":"application/octet-stream","size":1145,"digest":"sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"},"layers":[{"mediaType":"application/vnd.docker.image.rootfs.diff.tar.gzip","size":2319870,"digest":"sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"}]}
+{
+ "schemaVersion": 2,
+ "mediaType": "application/vnd.docker.distribution.manifest.v2+json",
+ "config": {
+ "mediaType": "application/octet-stream",
+ "size": 1145,
+ "digest": "sha256:d7a513a663c1a6dcdba9ed832ca53c02ac2af0c333322cd6ca92936d1d9917ac"
+ },
+ "layers": [
+ {
+ "mediaType": "application/vnd.docker.image.rootfs.diff.tar.gzip",
+ "size": 2319870,
+ "digest": "sha256:420890c9e918b6668faaedd9000e220190f2493b0693ee563ebd7b4cc754a57d"
+ }
+ ]
+}
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f6c1005d265..bb28866f010 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -174,51 +174,6 @@ describe ApplicationHelper do
end
end
- describe 'grouped_options_refs' do
- let(:options) { helper.grouped_options_refs }
- let(:project) { create(:project) }
-
- before do
- assign(:project, project)
-
- # Override Rails' grouped_options_for_select helper to just return the
- # first argument (`options`), since it's easier to work with than the
- # generated HTML.
- allow(helper).to receive(:grouped_options_for_select).
- and_wrap_original { |_, *args| args.first }
- end
-
- it 'includes a list of branch names' do
- expect(options[0][0]).to eq('Branches')
- expect(options[0][1]).to include('master', 'feature')
- end
-
- it 'includes a list of tag names' do
- expect(options[1][0]).to eq('Tags')
- expect(options[1][1]).to include('v1.0.0', 'v1.1.0')
- end
-
- it 'includes a specific commit ref if defined' do
- # Must be an instance variable
- ref = '2ed06dc41dbb5936af845b87d79e05bbf24c73b8'
- assign(:ref, ref)
-
- expect(options[2][0]).to eq('Commit')
- expect(options[2][1]).to eq([ref])
- end
-
- it 'sorts tags in a natural order' do
- # Stub repository.tag_names to make sure we get some valid testing data
- expect(project.repository).to receive(:tag_names).
- and_return(['v1.0.9', 'v1.0.10', 'v2.0', 'v3.1.4.2', 'v2.0rc1¿',
- 'v1.0.9a', 'v2.0-rc1', 'v2.0rc2'])
-
- expect(options[1][1]).
- to eq(['v3.1.4.2', 'v2.0', 'v2.0rc2', 'v2.0rc1¿', 'v2.0-rc1', 'v1.0.10',
- 'v1.0.9', 'v1.0.9a'])
- end
- end
-
describe 'simple_sanitize' do
let(:a_tag) { '<a href="#">Foo</a>' }
diff --git a/spec/helpers/members_helper_spec.rb b/spec/helpers/members_helper_spec.rb
index 7998209b7b0..f75fdb739f6 100644
--- a/spec/helpers/members_helper_spec.rb
+++ b/spec/helpers/members_helper_spec.rb
@@ -9,6 +9,54 @@ describe MembersHelper do
it { expect(action_member_permission(:admin, group_member)).to eq :admin_group_member }
end
+ describe '#default_show_roles' do
+ let(:user) { double }
+ let(:member) { build(:project_member) }
+
+ before do
+ allow(helper).to receive(:current_user).and_return(user)
+ allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(false)
+ allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(false)
+ allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(false)
+ end
+
+ context 'when the current cannot update, destroy or admin the passed member' do
+ it 'returns false' do
+ expect(helper.default_show_roles(member)).to be_falsy
+ end
+ end
+
+ context 'when the current can update the passed member' do
+ before do
+ allow(helper).to receive(:can?).with(user, :update_project_member, member).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+
+ context 'when the current can destroy the passed member' do
+ before do
+ allow(helper).to receive(:can?).with(user, :destroy_project_member, member).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+
+ context 'when the current can admin the passed member source' do
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_project_member, member.source).and_return(true)
+ end
+
+ it 'returns true' do
+ expect(helper.default_show_roles(member)).to be_truthy
+ end
+ end
+ end
+
describe '#remove_member_message' do
let(:requester) { build(:user) }
let(:project) { create(:project) }
diff --git a/spec/javascripts/issue_spec.js.coffee b/spec/javascripts/issue_spec.js.coffee
index ea27f36e9b5..71f0c1076c5 100644
--- a/spec/javascripts/issue_spec.js.coffee
+++ b/spec/javascripts/issue_spec.js.coffee
@@ -1,3 +1,4 @@
+#= require lib/text_utility
#= require issue
describe 'Issue', ->
@@ -38,7 +39,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeVisible()
expect($btnClose).toBeHidden()
expect($('div.status-box-closed')).toBeVisible()
@@ -50,7 +51,7 @@ describe 'reopen/close issue', ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://goesnowhere.nothing/whereami')
req.success saved: false
-
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
@@ -59,7 +60,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
@@ -73,7 +74,7 @@ describe 'reopen/close issue', ->
expect(req.type).toBe('PUT')
expect(req.url).toBe('http://goesnowhere.nothing/whereami')
req.error()
-
+
$btnClose = $('a.btn-close')
$btnReopen = $('a.btn-reopen')
$btnClose.attr('href','http://goesnowhere.nothing/whereami')
@@ -82,7 +83,7 @@ describe 'reopen/close issue', ->
expect(typeof $btnClose.prop('disabled')).toBe('undefined')
$btnClose.trigger('click')
-
+
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-closed')).toBeHidden()
@@ -105,4 +106,4 @@ describe 'reopen/close issue', ->
expect($btnReopen).toBeHidden()
expect($btnClose).toBeVisible()
expect($('div.status-box-open')).toBeVisible()
- expect($('div.status-box-closed')).toBeHidden() \ No newline at end of file
+ expect($('div.status-box-closed')).toBeHidden()
diff --git a/spec/lib/banzai/filter/abstract_link_filter_spec.rb b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
index 0c55d8e19da..1ee31a603e4 100644
--- a/spec/lib/banzai/filter/abstract_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/abstract_link_filter_spec.rb
@@ -8,7 +8,7 @@ describe Banzai::Filter::AbstractReferenceFilter do
doc = Nokogiri::HTML.fragment("#1 #{project.to_reference}#2")
filter = described_class.new(doc, project: project)
- expect(filter).to receive(:object_class).twice.and_return(Issue)
+ expect(filter).to receive(:object_class).exactly(4).times.and_return(Issue)
expect(filter).to receive(:object_sym).twice.and_return(:issue)
refs = filter.references_per_project
diff --git a/spec/lib/banzai/filter/issue_reference_filter_spec.rb b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
index 25f0bc2092f..5b63c946114 100644
--- a/spec/lib/banzai/filter/issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/issue_reference_filter_spec.rb
@@ -134,6 +134,12 @@ describe Banzai::Filter::IssueReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
+
+ it 'ignores out-of-bounds issue IDs on the referenced project' do
+ exp = act = "Fixed ##{Gitlab::Database::MAX_INT_VALUE + 1}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
end
context 'cross-project URL reference' do
diff --git a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
index 3185e41fe5c..805acf1c8b3 100644
--- a/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/merge_request_reference_filter_spec.rb
@@ -38,6 +38,12 @@ describe Banzai::Filter::MergeRequestReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq exp
end
+ it 'ignores out-of-bounds merge request IDs on the referenced project' do
+ exp = act = "Merge !#{Gitlab::Database::MAX_INT_VALUE + 1}"
+
+ expect(reference_filter(act).to_html).to eq exp
+ end
+
it 'includes a title attribute' do
doc = reference_filter("Merge #{reference}")
expect(doc.css('a').first.attr('title')).to eq "Merge Request: #{merge.title}"
diff --git a/spec/lib/banzai/filter/relative_link_filter_spec.rb b/spec/lib/banzai/filter/relative_link_filter_spec.rb
index 0e6685f0ffb..b9e4a4eaf0e 100644
--- a/spec/lib/banzai/filter/relative_link_filter_spec.rb
+++ b/spec/lib/banzai/filter/relative_link_filter_spec.rb
@@ -132,11 +132,8 @@ describe Banzai::Filter::RelativeLinkFilter, lib: true do
path = 'files/images/한글.png'
escaped = Addressable::URI.escape(path)
- # Stub these methods so the file doesn't actually need to be in the repo
- allow_any_instance_of(described_class).
- to receive(:file_exists?).and_return(true)
- allow_any_instance_of(described_class).
- to receive(:image?).with(path).and_return(true)
+ # Stub this method so the file doesn't actually need to be in the repo
+ allow_any_instance_of(described_class).to receive(:uri_type).and_return(:raw)
doc = filter(image(escaped))
expect(doc.at_css('img')['src']).to match '/raw/'
diff --git a/spec/lib/banzai/filter/wiki_link_filter_spec.rb b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
new file mode 100644
index 00000000000..92d88c4172c
--- /dev/null
+++ b/spec/lib/banzai/filter/wiki_link_filter_spec.rb
@@ -0,0 +1,26 @@
+require 'spec_helper'
+
+describe Banzai::Filter::WikiLinkFilter, lib: true do
+ include FilterSpecHelper
+
+ let(:namespace) { build_stubbed(:namespace, name: "wiki_link_ns") }
+ let(:project) { build_stubbed(:empty_project, :public, name: "wiki_link_project", namespace: namespace) }
+ let(:user) { double }
+ let(:wiki) { ProjectWiki.new(project, user) }
+
+ it "doesn't rewrite absolute links" do
+ filtered_link = filter("<a href='http://example.com:8000/'>Link</a>", project_wiki: wiki).children[0]
+ expect(filtered_link.attribute('href').value).to eq('http://example.com:8000/')
+ end
+
+ describe "invalid links" do
+ invalid_links = ["http://:8080", "http://", "http://:8080/path"]
+
+ invalid_links.each do |invalid_link|
+ it "doesn't rewrite invalid invalid_links like #{invalid_link}" do
+ filtered_link = filter("<a href='#{invalid_link}'>Link</a>", project_wiki: wiki).children[0]
+ expect(filtered_link.attribute('href').value).to eq(invalid_link)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
index 88814bc474d..659facd6c19 100644
--- a/spec/lib/gitlab/lfs/lfs_router_spec.rb
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -17,12 +17,15 @@ describe Gitlab::Lfs::Router, lib: true do
}
end
- let(:lfs_router_auth) { new_lfs_router(project, user) }
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
- let(:lfs_router_public_auth) { new_lfs_router(public_project, user) }
- let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) }
- let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) }
- let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) }
+ let(:lfs_router_auth) { new_lfs_router(project, user: user) }
+ let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) }
+ let(:lfs_router_noauth) { new_lfs_router(project) }
+ let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) }
+ let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) }
+ let(:lfs_router_public_noauth) { new_lfs_router(public_project) }
+ let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) }
+ let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) }
+ let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) }
let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
let(:sample_size) { 499013 }
@@ -80,6 +83,7 @@ describe Gitlab::Lfs::Router, lib: true do
context 'with required headers' do
before do
+ project.lfs_objects << lfs_object
env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
end
@@ -91,7 +95,6 @@ describe Gitlab::Lfs::Router, lib: true do
context 'when user has project access' do
before do
- project.lfs_objects << lfs_object
project.team << [user, :master]
end
@@ -104,6 +107,17 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
end
end
+
+ context 'when CI is authorized' do
+ it "responds with status 200" do
+ expect(lfs_router_ci_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with the file location" do
+ expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
+ expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
end
context 'without required headers' do
@@ -134,143 +148,145 @@ describe Gitlab::Lfs::Router, lib: true do
end
describe 'download' do
- describe 'when user is authenticated' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
- describe 'when user has download access' do
+ shared_examples 'an authorized requests' do
+ context 'when downloading an lfs object that is assigned to our project' do
before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :reporter]
+ project.lfs_objects << lfs_object
end
- context 'when downloading an lfs object that is assigned to our project' do
- before do
- project.lfs_objects << lfs_object
- end
-
- it 'responds with status 200 and href to download' do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it 'responds with status 200 and href to download' do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => @auth }
- }
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => auth }
}
- }])
- end
+ }
+ }])
end
+ end
- context 'when downloading an lfs object that is assigned to other project' do
- before do
- public_project.lfs_objects << lfs_object
- end
+ context 'when downloading an lfs object that is assigned to other project' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
- it 'responds with status 200 and error message' do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it 'responds with status 200 and error message' do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
+ expect(response_body).to eq('objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
end
+ end
- context 'when downloading a lfs object that does not exist' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ context 'when downloading a lfs object that does not exist' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
- it "responds with status 200 and error message" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it "responds with status 200 and error message" do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- }])
- end
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ }])
end
+ end
- context 'when downloading one new and one existing lfs object' do
- before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078
- },
- { 'oid' => sample_oid,
- 'size' => sample_size
- }
- ]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- project.lfs_objects << lfs_object
- end
+ context 'when downloading one new and one existing lfs object' do
+ before do
+ body = { 'operation' => 'download',
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ project.lfs_objects << lfs_object
+ end
- it "responds with status 200 with upload hypermedia link for the new object" do
- response = lfs_router_auth.try_call
- expect(response.first).to eq(200)
- response_body = ActiveSupport::JSON.decode(response.last.first)
+ it "responds with status 200 with upload hypermedia link for the new object" do
+ response = router.try_call
+ expect(response.first).to eq(200)
+ response_body = ActiveSupport::JSON.decode(response.last.first)
- expect(response_body).to eq('objects' => [
- { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
- 'size' => 1575078,
- 'error' => {
- 'code' => 404,
- 'message' => "Object does not exist on the server or you don't have permissions to access it",
- }
- },
- { 'oid' => sample_oid,
- 'size' => sample_size,
- 'actions' => {
- 'download' => {
- 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
- 'header' => { 'Authorization' => @auth }
- }
+ expect(response_body).to eq('objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078,
+ 'error' => {
+ 'code' => 404,
+ 'message' => "Object does not exist on the server or you don't have permissions to access it",
+ }
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size,
+ 'actions' => {
+ 'download' => {
+ 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}",
+ 'header' => { 'Authorization' => auth }
}
- }])
- end
+ }
+ }])
end
end
+ end
+
+ context 'when user is authenticated' do
+ let(:auth) { authorize(user) }
+
+ before do
+ env["HTTP_AUTHORIZATION"] = auth
+ project.team << [user, role]
+ end
+
+ it_behaves_like 'an authorized requests' do
+ let(:role) { :reporter }
+ let(:router) { lfs_router_auth }
+ end
context 'when user does is not member of the project' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :guest]
- end
+ let(:role) { :guest }
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
@@ -278,11 +294,7 @@ describe Gitlab::Lfs::Router, lib: true do
end
context 'when user does not have download access' do
- before do
- @auth = authorize(user)
- env["HTTP_AUTHORIZATION"] = @auth
- project.team << [user, :guest]
- end
+ let(:role) { :guest }
it 'responds with 403' do
expect(lfs_router_auth.try_call.first).to eq(403)
@@ -290,18 +302,19 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- context 'when user is not authenticated' do
+ context 'when CI is authorized' do
+ let(:auth) { 'gitlab-ci-token:password' }
+
before do
- body = { 'operation' => 'download',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }],
+ env["HTTP_AUTHORIZATION"] = auth
+ end
- }.to_json
- env['rack.input'] = StringIO.new(body)
+ it_behaves_like 'an authorized requests' do
+ let(:router) { lfs_router_ci_auth }
end
+ end
+ context 'when user is not authenticated' do
describe 'is accessing public project' do
before do
public_project.lfs_objects << lfs_object
@@ -338,17 +351,17 @@ describe Gitlab::Lfs::Router, lib: true do
end
describe 'upload' do
- describe 'when user is authenticated' do
- before do
- body = { 'operation' => 'upload',
- 'objects' => [
- { 'oid' => sample_oid,
- 'size' => sample_size
- }]
- }.to_json
- env['rack.input'] = StringIO.new(body)
- end
+ before do
+ body = { 'operation' => 'upload',
+ 'objects' => [
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+ describe 'when request is authenticated' do
describe 'when user has project push access' do
before do
@auth = authorize(user)
@@ -440,15 +453,15 @@ describe Gitlab::Lfs::Router, lib: true do
expect(lfs_router_auth.try_call.first).to eq(403)
end
end
- end
- context 'when user is not authenticated' do
- before do
- env['rack.input'] = StringIO.new(
- { 'objects' => [], 'operation' => 'upload' }.to_json
- )
+ context 'when CI is authorized' do
+ it 'responds with 401' do
+ expect(lfs_router_ci_auth.try_call.first).to eq(401)
+ end
end
+ end
+ context 'when user is not authenticated' do
context 'when user has push access' do
before do
project.team << [user, :master]
@@ -465,6 +478,18 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
end
+
+ context 'when CI is authorized' do
+ let(:auth) { 'gitlab-ci-token:password' }
+
+ before do
+ env["HTTP_AUTHORIZATION"] = auth
+ end
+
+ it "responds with status 403" do
+ expect(lfs_router_public_ci_auth.try_call.first).to eq(401)
+ end
+ end
end
describe 'unsupported' do
@@ -490,13 +515,68 @@ describe Gitlab::Lfs::Router, lib: true do
env['REQUEST_METHOD'] = 'PUT'
end
- describe 'to one project' do
- describe 'when user has push access to the project' do
+ shared_examples 'unauthorized' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
- project.team << [user, :master]
+ header_for_upload_authorize(router.project)
+ end
+
+ it 'responds with status 401' do
+ expect(router.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(router.project)
+ end
+
+ it 'responds with status 401' do
+ expect(router.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(router.try_call).to eq(nil)
+ end
+ end
+ end
+
+ shared_examples 'forbidden' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(router.project)
+ end
+
+ it 'responds with 403' do
+ expect(router.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(router.project)
+ end
+
+ it 'responds with 403' do
+ expect(router.try_call.first).to eq(403)
end
+ end
+ end
+
+ describe 'to one project' do
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :developer]
+ end
- describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(project)
@@ -524,100 +604,35 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- describe 'when user is unauthenticated' do
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+ describe 'and user does not have push access' do
+ let(:router) { lfs_router_auth }
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent with a malformed headers' do
- before do
- env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
- env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
- end
-
- it 'does not recognize it as a valid lfs command' do
- expect(lfs_router_noauth.try_call).to eq(nil)
- end
- end
+ it_behaves_like 'forbidden'
end
end
- describe 'and user does not have push access' do
- describe 'when user is authenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_auth.try_call.first).to eq(403)
- end
- end
- end
+ context 'when CI is authenticated' do
+ let(:router) { lfs_router_ci_auth }
- describe 'when user is unauthenticated' do
- let(:lfs_router_noauth) { new_lfs_router(project, nil) }
-
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(project)
- end
+ it_behaves_like 'unauthorized'
+ end
- it 'responds with 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
+ context 'for unauthenticated' do
+ let(:router) { new_lfs_router(project) }
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(project)
- end
-
- it 'responds with 401' do
- expect(lfs_router_noauth.try_call.first).to eq(401)
- end
- end
- end
+ it_behaves_like 'unauthorized'
end
end
- describe "to a forked project" do
+ describe 'to a forked project' do
let(:forked_project) { fork_project(public_project, user) }
- describe 'when user has push access to the project' do
- before do
- forked_project.team << [user_two, :master]
- end
+ describe 'when user is authenticated' do
+ describe 'when user has push access to the project' do
+ before do
+ forked_project.team << [user_two, :developer]
+ end
- describe 'when user is authenticated' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
header_for_upload_authorize(forked_project)
@@ -645,78 +660,28 @@ describe Gitlab::Lfs::Router, lib: true do
end
end
- describe 'when user is unauthenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with status 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
+ describe 'and user does not have push access' do
+ let(:router) { lfs_router_forked_auth }
- it 'responds with status 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
+ it_behaves_like 'forbidden'
end
end
- describe 'and user does not have push access' do
- describe 'when user is authenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
+ context 'when CI is authenticated' do
+ let(:router) { lfs_router_forked_ci_auth }
- it 'responds with 403' do
- expect(lfs_router_forked_auth.try_call.first).to eq(403)
- end
- end
-
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
-
- it 'responds with 403' do
- expect(lfs_router_forked_auth.try_call.first).to eq(403)
- end
- end
- end
-
- describe 'when user is unauthenticated' do
- context 'and request is sent by gitlab-workhorse to authorize the request' do
- before do
- header_for_upload_authorize(forked_project)
- end
-
- it 'responds with 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
+ it_behaves_like 'unauthorized'
+ end
- context 'and request is sent by gitlab-workhorse to finalize the upload' do
- before do
- headers_for_upload_finalize(forked_project)
- end
+ context 'for unauthenticated' do
+ let(:router) { lfs_router_forked_noauth }
- it 'responds with 401' do
- expect(lfs_router_forked_noauth.try_call.first).to eq(401)
- end
- end
- end
+ it_behaves_like 'unauthorized'
end
describe 'and second project not related to fork or a source project' do
let(:second_project) { create(:project) }
- let(:lfs_router_second_project) { new_lfs_router(second_project, user) }
+ let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) }
before do
public_project.lfs_objects << lfs_object
@@ -745,8 +710,8 @@ describe Gitlab::Lfs::Router, lib: true do
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
- def new_lfs_router(project, user)
- Gitlab::Lfs::Router.new(project, user, request)
+ def new_lfs_router(project, user: nil, ci: false)
+ Gitlab::Lfs::Router.new(project, user, ci, request)
end
def header_for_upload_authorize(project)
diff --git a/spec/lib/gitlab/gitignore_spec.rb b/spec/lib/gitlab/template/gitignore_spec.rb
index 72baa516cc4..bc0ec9325cc 100644
--- a/spec/lib/gitlab/gitignore_spec.rb
+++ b/spec/lib/gitlab/template/gitignore_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
-describe Gitlab::Gitignore do
- subject { Gitlab::Gitignore }
+describe Gitlab::Template::Gitignore do
+ subject { described_class }
describe '.all' do
it 'strips the gitignore suffix' do
@@ -24,7 +24,7 @@ describe Gitlab::Gitignore do
it 'returns the Gitignore object of a valid file' do
ruby = subject.find('Ruby')
- expect(ruby).to be_a Gitlab::Gitignore
+ expect(ruby).to be_a Gitlab::Template::Gitignore
expect(ruby.name).to eq('Ruby')
end
end
diff --git a/spec/mailers/notify_spec.rb b/spec/mailers/notify_spec.rb
index 1e6eb20ab39..ae55a01ebea 100644
--- a/spec/mailers/notify_spec.rb
+++ b/spec/mailers/notify_spec.rb
@@ -401,23 +401,56 @@ describe Notify do
end
describe 'project access requested' do
- let(:project) { create(:project) }
- let(:user) { create(:user) }
- let(:project_member) do
- project.request_access(user)
- project.members.request.find_by(user_id: user.id)
+ context 'for a project in a user namespace' do
+ let(:project) { create(:project).tap { |p| p.team << [p.owner, :master, p.owner] } }
+ let(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.id) }
+
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ to_emails = subject.header[:to].addrs
+ expect(to_emails.size).to eq(1)
+ expect(to_emails[0].address).to eq(project.members.owners_and_masters.first.user.notification_email)
+
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
end
- subject { Notify.member_access_requested_email('project', project_member.id) }
- it_behaves_like 'an email sent from GitLab'
- it_behaves_like 'it should not have Gmail Actions links'
- it_behaves_like "a user cannot unsubscribe through footer link"
+ 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(:user) { create(:user) }
+ let(:project_member) do
+ project.request_access(user)
+ project.members.request.find_by(user_id: user.id)
+ end
+ subject { Notify.member_access_requested_email('project', project_member.id) }
- it 'contains all the useful information' do
- is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
- is_expected.to have_body_text /#{project.name_with_namespace}/
- is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
- is_expected.to have_body_text /#{project_member.human_access}/
+ it_behaves_like 'an email sent from GitLab'
+ it_behaves_like 'it should not have Gmail Actions links'
+ it_behaves_like "a user cannot unsubscribe through footer link"
+
+ it 'contains all the useful information' do
+ to_emails = subject.header[:to].addrs
+ expect(to_emails.size).to eq(1)
+ expect(to_emails[0].address).to eq(group.members.owners_and_masters.first.user.notification_email)
+
+ is_expected.to have_subject "Request to join the #{project.name_with_namespace} project"
+ is_expected.to have_body_text /#{project.name_with_namespace}/
+ is_expected.to have_body_text /#{namespace_project_project_members_url(project.namespace, project)}/
+ is_expected.to have_body_text /#{project_member.human_access}/
+ end
end
end
diff --git a/spec/models/build_spec.rb b/spec/models/build_spec.rb
index 5d1fa8226e5..8154001cf46 100644
--- a/spec/models/build_spec.rb
+++ b/spec/models/build_spec.rb
@@ -2,7 +2,12 @@ require 'spec_helper'
describe Ci::Build, models: true do
let(:project) { create(:project) }
- let(:pipeline) { create(:ci_pipeline, project: project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project,
+ sha: project.commit.id)
+ end
+
let(:build) { create(:ci_build, pipeline: pipeline) }
it { is_expected.to validate_presence_of :ref }
@@ -36,32 +41,44 @@ describe Ci::Build, models: true do
subject { build.ignored? }
context 'if build is not allowed to fail' do
- before { build.allow_failure = false }
+ before do
+ build.allow_failure = false
+ end
context 'and build.status is success' do
- before { build.status = 'success' }
+ before do
+ build.status = 'success'
+ end
it { is_expected.to be_falsey }
end
context 'and build.status is failed' do
- before { build.status = 'failed' }
+ before do
+ build.status = 'failed'
+ end
it { is_expected.to be_falsey }
end
end
context 'if build is allowed to fail' do
- before { build.allow_failure = true }
+ before do
+ build.allow_failure = true
+ end
context 'and build.status is success' do
- before { build.status = 'success' }
+ before do
+ build.status = 'success'
+ end
it { is_expected.to be_falsey }
end
context 'and build.status is failed' do
- before { build.status = 'failed' }
+ before do
+ build.status = 'failed'
+ end
it { is_expected.to be_truthy }
end
@@ -75,7 +92,9 @@ describe Ci::Build, models: true do
context 'if build.trace contains text' do
let(:text) { 'example output' }
- before { build.trace = text }
+ before do
+ build.trace = text
+ end
it { is_expected.to include(text) }
it { expect(subject.length).to be >= text.length }
@@ -188,7 +207,9 @@ describe Ci::Build, models: true do
]
end
- before { build.update_attributes(stage: 'stage') }
+ before do
+ build.update_attributes(stage: 'stage')
+ end
it { is_expected.to eq(predefined_variables + yaml_variables) }
@@ -199,7 +220,9 @@ describe Ci::Build, models: true do
]
end
- before { build.update_attributes(tag: true) }
+ before do
+ build.update_attributes(tag: true)
+ end
it { is_expected.to eq(tag_variable + predefined_variables + yaml_variables) }
end
@@ -257,57 +280,6 @@ describe Ci::Build, models: true do
end
end
- describe '#can_be_served?' do
- let(:runner) { create(:ci_runner) }
-
- before { build.project.runners << runner }
-
- context 'when runner does not have tags' do
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'cannot handle build with tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
- end
- end
-
- context 'when runner has tags' do
- before { runner.tag_list = ['bb', 'cc'] }
-
- shared_examples 'tagged build picker' do
- it 'can handle build with matching tags' do
- build.tag_list = ['bb']
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it 'cannot handle build without matching tags' do
- build.tag_list = ['aa']
- expect(build.can_be_served?(runner)).to be_falsey
- end
- end
-
- context 'when runner can pick untagged jobs' do
- it 'can handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_truthy
- end
-
- it_behaves_like 'tagged build picker'
- end
-
- context 'when runner can not pick untagged jobs' do
- before { runner.run_untagged = false }
-
- it 'can not handle builds without tags' do
- expect(build.can_be_served?(runner)).to be_falsey
- end
-
- it_behaves_like 'tagged build picker'
- end
- end
- end
-
describe '#has_tags?' do
context 'when build has tags' do
subject { create(:ci_build, tag_list: ['tag']) }
@@ -348,7 +320,7 @@ describe Ci::Build, models: true do
end
it 'that cannot handle build' do
- expect_any_instance_of(Ci::Build).to receive(:can_be_served?).and_return(false)
+ expect_any_instance_of(Ci::Runner).to receive(:can_pick?).and_return(false)
is_expected.to be_falsey
end
@@ -360,7 +332,9 @@ describe Ci::Build, models: true do
%w(pending).each do |state|
context "if commit_status.status is #{state}" do
- before { build.status = state }
+ before do
+ build.status = state
+ end
it { is_expected.to be_truthy }
@@ -379,7 +353,9 @@ describe Ci::Build, models: true do
%w(success failed canceled running).each do |state|
context "if commit_status.status is #{state}" do
- before { build.status = state }
+ before do
+ build.status = state
+ end
it { is_expected.to be_falsey }
end
@@ -390,7 +366,10 @@ describe Ci::Build, models: true do
subject { build.artifacts? }
context 'artifacts archive does not exist' do
- before { build.update_attributes(artifacts_file: nil) }
+ before do
+ build.update_attributes(artifacts_file: nil)
+ end
+
it { is_expected.to be_falsy }
end
@@ -623,7 +602,9 @@ describe Ci::Build, models: true do
let!(:build) { create(:ci_build, :trace, :success, :artifacts) }
describe '#erase' do
- before { build.erase(erased_by: user) }
+ before do
+ build.erase(erased_by: user)
+ end
context 'erased by user' do
let!(:user) { create(:user, username: 'eraser') }
@@ -660,7 +641,9 @@ describe Ci::Build, models: true do
end
context 'build has been erased' do
- before { build.erase }
+ before do
+ build.erase
+ end
it { is_expected.to be true }
end
@@ -668,7 +651,9 @@ describe Ci::Build, models: true do
context 'metadata and build trace are not available' do
let!(:build) { create(:ci_build, :success, :artifacts) }
- before { build.remove_artifacts_metadata! }
+ before do
+ build.remove_artifacts_metadata!
+ end
describe '#erase' do
it 'should not raise error' do
@@ -678,4 +663,10 @@ describe Ci::Build, models: true do
end
end
end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(build.commit).to eq project.commit
+ end
+ end
end
diff --git a/spec/models/ci/runner_spec.rb b/spec/models/ci/runner_spec.rb
index 5d04d8ffcff..ef65eb99328 100644
--- a/spec/models/ci/runner_spec.rb
+++ b/spec/models/ci/runner_spec.rb
@@ -20,34 +20,36 @@ describe Ci::Runner, models: true do
end
describe '#display_name' do
- it 'should return the description if it has a value' do
+ it 'returns the description if it has a value' do
runner = FactoryGirl.build(:ci_runner, description: 'Linux/Ruby-1.9.3-p448')
expect(runner.display_name).to eq 'Linux/Ruby-1.9.3-p448'
end
- it 'should return the token if it does not have a description' do
+ it 'returns the token if it does not have a description' do
runner = FactoryGirl.create(:ci_runner)
expect(runner.display_name).to eq runner.description
end
- it 'should return the token if the description is an empty string' do
+ it 'returns the token if the description is an empty string' do
runner = FactoryGirl.build(:ci_runner, description: '', token: 'token')
expect(runner.display_name).to eq runner.token
end
end
- describe :assign_to do
+ describe '#assign_to' do
let!(:project) { FactoryGirl.create :empty_project }
let!(:shared_runner) { FactoryGirl.create(:ci_runner, :shared) }
- before { shared_runner.assign_to(project) }
+ before do
+ shared_runner.assign_to(project)
+ end
it { expect(shared_runner).to be_specific }
it { expect(shared_runner.projects).to eq([project]) }
it { expect(shared_runner.only_for?(project)).to be_truthy }
end
- describe :online do
+ describe '.online' do
subject { Ci::Runner.online }
before do
@@ -58,60 +60,269 @@ describe Ci::Runner, models: true do
it { is_expected.to eq([@runner2])}
end
- describe :online? do
+ describe '#online?' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared) }
subject { runner.online? }
context 'never contacted' do
- before { runner.contacted_at = nil }
+ before do
+ runner.contacted_at = nil
+ end
it { is_expected.to be_falsey }
end
context 'contacted long time ago time' do
- before { runner.contacted_at = 1.year.ago }
+ before do
+ runner.contacted_at = 1.year.ago
+ end
it { is_expected.to be_falsey }
end
context 'contacted 1s ago' do
- before { runner.contacted_at = 1.second.ago }
+ before do
+ runner.contacted_at = 1.second.ago
+ end
it { is_expected.to be_truthy }
end
end
- describe :status do
+ describe '#can_pick?' do
+ let(:project) { create(:project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, pipeline: pipeline) }
+ let(:runner) { create(:ci_runner) }
+
+ before do
+ build.project.runners << runner
+ end
+
+ context 'when runner does not have tags' do
+ it 'can handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it 'cannot handle build with tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when runner has tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ end
+
+ shared_examples 'tagged build picker' do
+ it 'can handle build with matching tags' do
+ build.tag_list = ['bb']
+
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it 'cannot handle build without matching tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when runner can pick untagged jobs' do
+ it 'can handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it_behaves_like 'tagged build picker'
+ end
+
+ context 'when runner cannot pick untagged jobs' do
+ before do
+ runner.run_untagged = false
+ end
+
+ it 'cannot handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+
+ it_behaves_like 'tagged build picker'
+ end
+ end
+
+ context 'when runner is locked' do
+ before do
+ runner.locked = true
+ end
+
+ shared_examples 'locked build picker' do
+ context 'when runner cannot pick untagged jobs' do
+ before do
+ runner.run_untagged = false
+ end
+
+ it 'cannot handle builds without tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ end
+
+ it 'cannot handle it for builds without matching tags' do
+ build.tag_list = ['aa']
+
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+ end
+
+ context 'when serving the same project' do
+ it 'can handle it' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+
+ it_behaves_like 'locked build picker'
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ build.tag_list = ['bb']
+ end
+
+ it 'can handle it for matching tags' do
+ expect(runner.can_pick?(build)).to be_truthy
+ end
+ end
+ end
+
+ context 'serving a different project' do
+ before do
+ runner.runner_projects.destroy_all
+ end
+
+ it 'cannot handle it' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+
+ it_behaves_like 'locked build picker'
+
+ context 'when having runner tags' do
+ before do
+ runner.tag_list = ['bb', 'cc']
+ build.tag_list = ['bb']
+ end
+
+ it 'cannot handle it for matching tags' do
+ expect(runner.can_pick?(build)).to be_falsey
+ end
+ end
+ end
+ end
+ end
+
+ describe '#status' do
let(:runner) { FactoryGirl.create(:ci_runner, :shared, contacted_at: 1.second.ago) }
subject { runner.status }
context 'never connected' do
- before { runner.contacted_at = nil }
+ before do
+ runner.contacted_at = nil
+ end
it { is_expected.to eq(:not_connected) }
end
context 'contacted 1s ago' do
- before { runner.contacted_at = 1.second.ago }
+ before do
+ runner.contacted_at = 1.second.ago
+ end
it { is_expected.to eq(:online) }
end
context 'contacted long time ago' do
- before { runner.contacted_at = 1.year.ago }
+ before do
+ runner.contacted_at = 1.year.ago
+ end
it { is_expected.to eq(:offline) }
end
context 'inactive' do
- before { runner.active = false }
+ before do
+ runner.active = false
+ end
it { is_expected.to eq(:paused) }
end
end
+ describe '.assignable_for' do
+ let(:runner) { create(:ci_runner) }
+ let(:project) { create(:project) }
+ let(:another_project) { create(:project) }
+
+ before do
+ project.runners << runner
+ end
+
+ context 'with shared runners' do
+ before do
+ runner.update(is_shared: true)
+ end
+
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does not give shared runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+
+ context 'with unlocked runner' do
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does give a specific runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to contain_exactly(runner) }
+ end
+ end
+
+ context 'with locked runner' do
+ before do
+ runner.update(locked: true)
+ end
+
+ context 'does not give owned runner' do
+ subject { Ci::Runner.assignable_for(project) }
+
+ it { is_expected.to be_empty }
+ end
+
+ context 'does not give a locked runner' do
+ subject { Ci::Runner.assignable_for(another_project) }
+
+ it { is_expected.to be_empty }
+ end
+ end
+ end
+
describe "belongs_to_one_project?" do
it "returns false if there are two projects runner assigned to" do
runner = FactoryGirl.create(:ci_runner)
diff --git a/spec/models/commit_spec.rb b/spec/models/commit_spec.rb
index beca8708c9d..ba02d5fe977 100644
--- a/spec/models/commit_spec.rb
+++ b/spec/models/commit_spec.rb
@@ -207,4 +207,16 @@ eos
expect(commit.participants).to include(note1.author, note2.author)
end
end
+
+ describe '#uri_type' do
+ it 'returns the URI type at the given path' do
+ expect(commit.uri_type('files/html')).to be(:tree)
+ expect(commit.uri_type('files/images/logo-black.png')).to be(:raw)
+ expect(commit.uri_type('files/js/application.js')).to be(:blob)
+ end
+
+ it "returns nil if the path doesn't exists" do
+ expect(commit.uri_type('this/path/doesnt/exist')).to be_nil
+ end
+ end
end
diff --git a/spec/models/commit_status_spec.rb b/spec/models/commit_status_spec.rb
index 8fb605fff8a..96397d7c8a9 100644
--- a/spec/models/commit_status_spec.rb
+++ b/spec/models/commit_status_spec.rb
@@ -1,8 +1,13 @@
require 'spec_helper'
describe CommitStatus, models: true do
- let(:pipeline) { FactoryGirl.create :ci_pipeline }
- let(:commit_status) { FactoryGirl.create :commit_status, pipeline: pipeline }
+ let(:project) { create(:project) }
+
+ let(:pipeline) do
+ create(:ci_pipeline, project: project, sha: project.commit.id)
+ end
+
+ let(:commit_status) { create(:commit_status, pipeline: pipeline) }
it { is_expected.to belong_to(:pipeline) }
it { is_expected.to belong_to(:user) }
@@ -13,7 +18,7 @@ describe CommitStatus, models: true do
it { is_expected.to delegate_method(:sha).to(:pipeline) }
it { is_expected.to delegate_method(:short_sha).to(:pipeline) }
-
+
it { is_expected.to respond_to :success? }
it { is_expected.to respond_to :failed? }
it { is_expected.to respond_to :running? }
@@ -116,7 +121,7 @@ describe CommitStatus, models: true do
it { is_expected.to be > 0.0 }
end
end
-
+
describe :latest do
subject { CommitStatus.latest.order(:id) }
@@ -198,4 +203,10 @@ describe CommitStatus, models: true do
end
end
end
+
+ describe '#commit' do
+ it 'returns commit pipeline has been created for' do
+ expect(commit_status.commit).to eq project.commit
+ end
+ end
end
diff --git a/spec/models/concerns/participable_spec.rb b/spec/models/concerns/participable_spec.rb
index 7e4ea0f2d66..a9f4ef9ee5e 100644
--- a/spec/models/concerns/participable_spec.rb
+++ b/spec/models/concerns/participable_spec.rb
@@ -37,6 +37,16 @@ describe Participable, models: true do
expect(participants).to include(user3)
end
+ it 'caches the raw list of participants' do
+ instance = model.new
+ user1 = build(:user)
+
+ expect(instance).to receive(:raw_participants).once
+
+ instance.participants(user1)
+ instance.participants(user1)
+ end
+
it 'supports attributes returning another Participable' do
other_model = Class.new { include Participable }
diff --git a/spec/models/key_spec.rb b/spec/models/key_spec.rb
index 26fbedbef2f..49cf3d8633a 100644
--- a/spec/models/key_spec.rb
+++ b/spec/models/key_spec.rb
@@ -26,7 +26,7 @@ describe Key, models: true do
end
end
- context "validation of uniqueness" do
+ context "validation of uniqueness (based on fingerprint uniqueness)" do
let(:user) { create(:user) }
it "accepts the key once" do
diff --git a/spec/models/member_spec.rb b/spec/models/member_spec.rb
index 3ed3202ac6c..e9134a3d283 100644
--- a/spec/models/member_spec.rb
+++ b/spec/models/member_spec.rb
@@ -134,18 +134,6 @@ describe Member, models: true do
it { is_expected.to respond_to(:user_email) }
end
- describe 'Callbacks' do
- describe 'after_destroy :post_decline_request, if: :request?' do
- let(:member) { create(:project_member, requested_at: Time.now.utc) }
-
- it 'calls #post_decline_request' do
- expect(member).to receive(:post_decline_request)
-
- member.destroy
- end
- end
- end
-
describe ".add_user" do
let!(:user) { create(:user) }
let(:project) { create(:project) }
diff --git a/spec/models/members/group_member_spec.rb b/spec/models/members/group_member_spec.rb
index eeb74a462ac..18439cac2a4 100644
--- a/spec/models/members/group_member_spec.rb
+++ b/spec/models/members/group_member_spec.rb
@@ -61,16 +61,6 @@ describe GroupMember, models: true do
end
end
- describe '#post_decline_request' do
- it 'calls NotificationService.decline_group_access_request' do
- member = create(:group_member, user: build_stubbed(:user), requested_at: Time.now)
-
- expect_any_instance_of(NotificationService).to receive(:decline_group_access_request)
-
- member.__send__(:post_decline_request)
- end
- end
-
describe '#real_source_type' do
subject { create(:group_member).real_source_type }
diff --git a/spec/models/members/project_member_spec.rb b/spec/models/members/project_member_spec.rb
index 1e466f9c620..bbf65edb27c 100644
--- a/spec/models/members/project_member_spec.rb
+++ b/spec/models/members/project_member_spec.rb
@@ -152,15 +152,5 @@ describe ProjectMember, models: true do
member.__send__(:after_accept_request)
end
end
-
- describe '#post_decline_request' do
- it 'calls NotificationService.decline_project_access_request' do
- member = create(:project_member, user: build_stubbed(:user), requested_at: Time.now)
-
- expect_any_instance_of(NotificationService).to receive(:decline_project_access_request)
-
- member.__send__(:post_decline_request)
- end
- end
end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index 4e24e89b008..df336a6effe 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -12,5 +12,30 @@ RSpec.describe NotificationSetting, type: :model do
it { is_expected.to validate_presence_of(:user) }
it { is_expected.to validate_presence_of(:level) }
it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_id, :source_type]).with_message(/already exists in source/) }
+
+ context "events" do
+ let(:user) { create(:user) }
+ let(:notification_setting) { NotificationSetting.new(source_id: 1, source_type: 'Project', user_id: user.id) }
+
+ before do
+ notification_setting.level = "custom"
+ notification_setting.new_note = "true"
+ notification_setting.new_issue = 1
+ notification_setting.close_issue = "1"
+ notification_setting.merge_merge_request = "t"
+ notification_setting.close_merge_request = "nil"
+ notification_setting.reopen_merge_request = "false"
+ notification_setting.save
+ end
+
+ it "parses boolean before saving" do
+ expect(notification_setting.new_note).to eq(true)
+ expect(notification_setting.new_issue).to eq(true)
+ expect(notification_setting.close_issue).to eq(true)
+ expect(notification_setting.merge_merge_request).to eq(true)
+ expect(notification_setting.close_merge_request).to eq(false)
+ expect(notification_setting.reopen_merge_request).to eq(false)
+ end
+ end
end
end
diff --git a/spec/requests/api/builds_spec.rb b/spec/requests/api/builds_spec.rb
index ac85f340922..47e9253a10c 100644
--- a/spec/requests/api/builds_spec.rb
+++ b/spec/requests/api/builds_spec.rb
@@ -9,8 +9,8 @@ describe API::API, api: true do
let!(:project) { create(:project, creator_id: user.id) }
let!(:developer) { create(:project_member, :developer, user: user, project: project) }
let!(:reporter) { create(:project_member, :reporter, user: user2, project: project) }
- let(:pipeline) { create(:ci_pipeline, project: project)}
- let(:build) { create(:ci_build, pipeline: pipeline) }
+ let!(:pipeline) { create(:ci_pipeline, project: project, sha: project.commit.id) }
+ let!(:build) { create(:ci_build, pipeline: pipeline) }
describe 'GET /projects/:id/builds ' do
let(:query) { '' }
@@ -23,6 +23,11 @@ describe API::API, api: true do
expect(json_response).to be_an Array
end
+ it 'returns correct values' do
+ expect(json_response).not_to be_empty
+ expect(json_response.first['commit']['id']).to eq project.commit.id
+ end
+
context 'filter project with one scope element' do
let(:query) { 'scope=pending' }
@@ -132,7 +137,7 @@ describe API::API, api: true do
describe 'GET /projects/:id/builds/:build_id/trace' do
let(:build) { create(:ci_build, :trace, pipeline: pipeline) }
-
+
before { get api("/projects/#{project.id}/builds/#{build.id}/trace", api_user) }
context 'authorized user' do
diff --git a/spec/requests/api/gitignores_spec.rb b/spec/requests/api/gitignores_spec.rb
deleted file mode 100644
index aab2d8c81b9..00000000000
--- a/spec/requests/api/gitignores_spec.rb
+++ /dev/null
@@ -1,29 +0,0 @@
-require 'spec_helper'
-
-describe API::Gitignores, api: true do
- include ApiHelpers
-
- describe 'Entity Gitignore' do
- before { get api('/gitignores/Ruby') }
-
- it { expect(json_response['name']).to eq('Ruby') }
- it { expect(json_response['content']).to include('*.gem') }
- end
-
- describe 'Entity GitignoresList' do
- before { get api('/gitignores') }
-
- it { expect(json_response.first['name']).not_to be_nil }
- it { expect(json_response.first['content']).to be_nil }
- end
-
- describe 'GET /gitignores' do
- it 'returns a list of available license templates' do
- get api('/gitignores')
-
- expect(response.status).to eq(200)
- expect(json_response).to be_an Array
- expect(json_response.size).to be > 15
- end
- end
-end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index f167813e07d..01eb4b44b83 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -428,8 +428,9 @@ describe API::API, api: true do
describe 'permissions' do
context 'all projects' do
- it 'Contains permission information' do
- project.team << [user, :master]
+ before { project.team << [user, :master] }
+
+ it 'contains permission information' do
get api("/projects", user)
expect(response.status).to eq(200)
@@ -440,7 +441,7 @@ describe API::API, api: true do
end
context 'personal project' do
- it 'Sets project access and returns 200' do
+ it 'sets project access and returns 200' do
project.team << [user, :master]
get api("/projects/#{project.id}", user)
@@ -452,9 +453,11 @@ describe API::API, api: true do
end
context 'group project' do
+ let(:project2) { create(:project, group: create(:group)) }
+
+ before { project2.group.add_owner(user) }
+
it 'should set the owner and return 200' do
- project2 = create(:project, group: create(:group))
- project2.group.add_owner(user)
get api("/projects/#{project2.id}", user)
expect(response.status).to eq(200)
diff --git a/spec/requests/api/runners_spec.rb b/spec/requests/api/runners_spec.rb
index 73ae8ef631c..b4c826522a5 100644
--- a/spec/requests/api/runners_spec.rb
+++ b/spec/requests/api/runners_spec.rb
@@ -187,14 +187,16 @@ describe API::Runners, api: true do
update_runner(shared_runner.id, admin, description: "#{description}_updated",
active: !active,
tag_list: ['ruby2.1', 'pgsql', 'mysql'],
- run_untagged: 'false')
+ run_untagged: 'false',
+ locked: 'true')
shared_runner.reload
expect(response.status).to eq(200)
expect(shared_runner.description).to eq("#{description}_updated")
expect(shared_runner.active).to eq(!active)
expect(shared_runner.tag_list).to include('ruby2.1', 'pgsql', 'mysql')
- expect(shared_runner.run_untagged?).to be false
+ expect(shared_runner.run_untagged?).to be(false)
+ expect(shared_runner.locked?).to be(true)
end
end
@@ -360,11 +362,13 @@ describe API::Runners, api: true do
describe 'POST /projects/:id/runners' do
context 'authorized user' do
- it 'should enable specific runner' do
- specific_runner2 = create(:ci_runner).tap do |runner|
+ let(:specific_runner2) do
+ create(:ci_runner).tap do |runner|
create(:ci_runner_project, runner: runner, project: project2)
end
+ end
+ it 'should enable specific runner' do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
end.to change{ project.runners.count }.by(+1)
@@ -375,7 +379,17 @@ describe API::Runners, api: true do
expect do
post api("/projects/#{project.id}/runners", user), runner_id: specific_runner.id
end.to change{ project.runners.count }.by(0)
- expect(response.status).to eq(201)
+ expect(response.status).to eq(409)
+ end
+
+ it 'should not enable locked runner' do
+ specific_runner2.update(locked: true)
+
+ expect do
+ post api("/projects/#{project.id}/runners", user), runner_id: specific_runner2.id
+ end.to change{ project.runners.count }.by(0)
+
+ expect(response.status).to eq(403)
end
it 'should not enable shared runner' do
diff --git a/spec/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
new file mode 100644
index 00000000000..a6d5ade3013
--- /dev/null
+++ b/spec/requests/api/templates_spec.rb
@@ -0,0 +1,52 @@
+require 'spec_helper'
+
+describe API::Templates, api: true do
+ include ApiHelpers
+
+ describe 'the Template Entity' do
+ before { get api('/gitignores/Ruby') }
+
+ it { expect(json_response['name']).to eq('Ruby') }
+ it { expect(json_response['content']).to include('*.gem') }
+ end
+
+ describe 'the TemplateList Entity' do
+ before { get api('/gitignores') }
+
+ it { expect(json_response.first['name']).not_to be_nil }
+ it { expect(json_response.first['content']).to be_nil }
+ end
+
+ context 'requesting gitignores' do
+ describe 'GET /gitignores' do
+ it 'returns a list of available gitignore templates' do
+ get api('/gitignores')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.size).to be > 15
+ end
+ end
+ end
+
+ context 'requesting gitlab-ci-ymls' do
+ describe 'GET /gitlab_ci_ymls' do
+ it 'returns a list of available gitlab_ci_ymls' do
+ get api('/gitlab_ci_ymls')
+
+ expect(response.status).to eq(200)
+ expect(json_response).to be_an Array
+ expect(json_response.first['name']).not_to be_nil
+ end
+ end
+ end
+
+ describe 'GET /gitlab_ci_ymls/Ruby' do
+ it 'adds a disclaimer on the top' do
+ get api('/gitlab_ci_ymls/Ruby')
+
+ expect(response.status).to eq(200)
+ expect(json_response['content']).to start_with("# This file is a template,")
+ end
+ end
+end
diff --git a/spec/services/members/destroy_service_spec.rb b/spec/services/members/destroy_service_spec.rb
new file mode 100644
index 00000000000..2395445e7fd
--- /dev/null
+++ b/spec/services/members/destroy_service_spec.rb
@@ -0,0 +1,71 @@
+require 'spec_helper'
+
+describe Members::DestroyService, services: true do
+ let(:user) { create(:user) }
+ let(:project) { create(:project) }
+ let!(:member) { create(:project_member, source: project) }
+
+ context 'when member is nil' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not destroy the member' do
+ expect { destroy_member(nil, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when current user cannot destroy the given member' do
+ before do
+ project.team << [user, :developer]
+ end
+
+ it 'does not destroy the member' do
+ expect { destroy_member(member, user) }.to raise_error(Gitlab::Access::AccessDeniedError)
+ end
+ end
+
+ context 'when current user can destroy the given member' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it 'destroys the member' do
+ destroy_member(member, user)
+
+ expect(member).to be_destroyed
+ end
+
+ context 'when the given member is a requester' do
+ before do
+ member.update_column(:requested_at, Time.now)
+ end
+
+ it 'calls Member#after_decline_request' do
+ expect_any_instance_of(NotificationService).to receive(:decline_access_request).with(member)
+
+ destroy_member(member, user)
+ 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
+ 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)
+
+ destroy_member(member, member.user)
+ end
+ end
+ end
+ end
+
+ def destroy_member(member, user)
+ Members::DestroyService.new(member, user).execute
+ end
+end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index e871a103d42..776a6ab5edb 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -46,6 +46,8 @@ describe NotificationService, services: true do
project.team << [issue.assignee, :master]
project.team << [note.author, :master]
create(:note_on_issue, noteable: issue, project_id: issue.project_id, note: '@subscribed_participant cc this guy')
+ update_custom_notification(:new_note, @u_guest_custom, project)
+ update_custom_notification(:new_note, @u_custom_global)
end
describe :new_note do
@@ -53,7 +55,7 @@ describe NotificationService, services: true do
add_users_with_subscription(note.project, issue)
# Ensure create SentNotification by noteable = issue 6 times, not noteable = note
- expect(SentNotification).to receive(:record).with(issue, any_args).exactly(7).times
+ expect(SentNotification).to receive(:record).with(issue, any_args).exactly(8).times
ActionMailer::Base.deliveries.clear
@@ -62,10 +64,12 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(note.noteable.author)
should_email(note.noteable.assignee)
+ should_email(@u_custom_global)
should_email(@u_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@subscribed_participant)
+ should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(note.author)
should_not_email(@u_participating)
@@ -103,10 +107,12 @@ describe NotificationService, services: true do
before do
note.project.namespace_id = group.id
note.project.group.add_user(@u_watcher, GroupMember::MASTER)
+ note.project.group.add_user(@u_custom_global, GroupMember::MASTER)
note.project.save
@u_watcher.notification_settings_for(note.project).participating!
@u_watcher.notification_settings_for(note.project.group).global!
+ update_custom_notification(:new_note, @u_custom_global)
ActionMailer::Base.deliveries.clear
end
@@ -116,6 +122,8 @@ describe NotificationService, services: true do
should_email(note.noteable.author)
should_email(note.noteable.assignee)
should_email(@u_mentioned)
+ should_email(@u_custom_global)
+ should_not_email(@u_guest_custom)
should_not_email(@u_guest_watcher)
should_not_email(@u_watcher)
should_not_email(note.author)
@@ -136,6 +144,7 @@ describe NotificationService, services: true do
let(:admin) { create(:admin) }
let(:confidential_issue) { create(:issue, :confidential, project: project, author: author, assignee: assignee) }
let(:note) { create(:note_on_issue, noteable: confidential_issue, project: project, note: "#{author.to_reference} #{assignee.to_reference} #{non_member.to_reference} #{member.to_reference} #{admin.to_reference}") }
+ let(:guest_watcher) { create_user_with_notification(:watch, "guest-watcher-confidential") }
it 'filters out users that can not read the issue' do
project.team << [member, :developer]
@@ -149,6 +158,7 @@ describe NotificationService, services: true do
should_not_email(non_member)
should_not_email(guest)
+ should_not_email(guest_watcher)
should_email(author)
should_email(assignee)
should_email(member)
@@ -223,6 +233,9 @@ describe NotificationService, services: true do
should_email(member)
end
+ # it emails custom global users on mention
+ should_email(@u_custom_global)
+
should_email(@u_guest_watcher)
should_email(note.noteable.author)
should_not_email(note.author)
@@ -241,13 +254,16 @@ describe NotificationService, services: true do
build_team(note.project)
ActionMailer::Base.deliveries.clear
allow_any_instance_of(Commit).to receive(:author).and_return(@u_committer)
+ update_custom_notification(:new_note, @u_guest_custom, project)
+ update_custom_notification(:new_note, @u_custom_global)
end
describe '#new_note, #perform_enqueued_jobs' do
it do
notification.new_note(note)
-
should_email(@u_guest_watcher)
+ should_email(@u_custom_global)
+ should_email(@u_guest_custom)
should_email(@u_committer)
should_email(@u_watcher)
should_not_email(@u_mentioned)
@@ -288,6 +304,8 @@ describe NotificationService, services: true do
build_team(issue.project)
add_users_with_subscription(issue.project, issue)
ActionMailer::Base.deliveries.clear
+ update_custom_notification(:new_issue, @u_guest_custom, project)
+ update_custom_notification(:new_issue, @u_custom_global)
end
describe '#new_issue' do
@@ -297,6 +315,8 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_not_email(@u_mentioned)
should_not_email(@u_participating)
@@ -345,6 +365,7 @@ describe NotificationService, services: true do
notification.new_issue(confidential_issue, @u_disabled)
+ should_not_email(@u_guest_watcher)
should_not_email(non_member)
should_not_email(author)
should_not_email(guest)
@@ -356,12 +377,20 @@ describe NotificationService, services: true do
end
describe '#reassigned_issue' do
+
+ before do
+ update_custom_notification(:reassign_issue, @u_guest_custom, project)
+ update_custom_notification(:reassign_issue, @u_custom_global)
+ end
+
it 'emails new assignee' do
notification.reassigned_issue(issue, @u_disabled)
should_email(issue.assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_not_email(@unsubscriber)
@@ -378,8 +407,10 @@ describe NotificationService, services: true do
should_email(@u_mentioned)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -394,8 +425,10 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -410,8 +443,10 @@ describe NotificationService, services: true do
should_email(issue.assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -425,8 +460,10 @@ describe NotificationService, services: true do
expect(issue.assignee).to be @u_mentioned
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
should_email(@u_participant_mentioned)
should_email(@subscriber)
+ should_email(@u_custom_global)
should_not_email(issue.assignee)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
@@ -529,6 +566,12 @@ describe NotificationService, services: true do
end
describe '#close_issue' do
+
+ before do
+ update_custom_notification(:close_issue, @u_guest_custom, project)
+ update_custom_notification(:close_issue, @u_custom_global)
+ end
+
it 'should sent email to issue assignee and issue author' do
notification.close_issue(issue, @u_disabled)
@@ -536,6 +579,8 @@ describe NotificationService, services: true do
should_email(issue.author)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
@@ -575,6 +620,11 @@ describe NotificationService, services: true do
end
describe '#reopen_issue' do
+ before do
+ update_custom_notification(:reopen_issue, @u_guest_custom, project)
+ update_custom_notification(:reopen_issue, @u_custom_global)
+ end
+
it 'should send email to issue assignee and issue author' do
notification.reopen_issue(issue, @u_disabled)
@@ -582,6 +632,8 @@ describe NotificationService, services: true do
should_email(issue.author)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
@@ -631,6 +683,11 @@ describe NotificationService, services: true do
end
describe '#new_merge_request' do
+ before do
+ update_custom_notification(:new_merge_request, @u_guest_custom, project)
+ update_custom_notification(:new_merge_request, @u_custom_global)
+ end
+
it do
notification.new_merge_request(merge_request, @u_disabled)
@@ -639,6 +696,8 @@ describe NotificationService, services: true do
should_email(@watcher_and_subscriber)
should_email(@u_participant_mentioned)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_not_email(@u_participating)
should_not_email(@u_disabled)
should_not_email(@u_lazy_participant)
@@ -685,6 +744,11 @@ describe NotificationService, services: true do
end
describe '#reassigned_merge_request' do
+ before do
+ update_custom_notification(:reassign_merge_request, @u_guest_custom, project)
+ update_custom_notification(:reassign_merge_request, @u_custom_global)
+ end
+
it do
notification.reassigned_merge_request(merge_request, merge_request.author)
@@ -694,6 +758,8 @@ describe NotificationService, services: true do
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -761,12 +827,19 @@ describe NotificationService, services: true do
end
describe '#closed_merge_request' do
+ before do
+ update_custom_notification(:close_merge_request, @u_guest_custom, project)
+ update_custom_notification(:close_merge_request, @u_custom_global)
+ end
+
it do
notification.close_mr(merge_request, @u_disabled)
should_email(merge_request.assignee)
should_email(@u_watcher)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_email(@u_participant_mentioned)
should_email(@subscriber)
should_email(@watcher_and_subscriber)
@@ -807,6 +880,12 @@ describe NotificationService, services: true do
end
describe '#merged_merge_request' do
+
+ before do
+ update_custom_notification(:merge_merge_request, @u_guest_custom, project)
+ update_custom_notification(:merge_merge_request, @u_custom_global)
+ end
+
it do
notification.merge_mr(merge_request, @u_disabled)
@@ -816,6 +895,8 @@ describe NotificationService, services: true do
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@u_guest_watcher)
+ should_email(@u_custom_global)
+ should_email(@u_guest_custom)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -853,6 +934,11 @@ describe NotificationService, services: true do
end
describe '#reopen_merge_request' do
+ before do
+ update_custom_notification(:reopen_merge_request, @u_guest_custom, project)
+ update_custom_notification(:reopen_merge_request, @u_custom_global)
+ end
+
it do
notification.reopen_mr(merge_request, @u_disabled)
@@ -862,6 +948,8 @@ describe NotificationService, services: true do
should_email(@subscriber)
should_email(@watcher_and_subscriber)
should_email(@u_guest_watcher)
+ should_email(@u_guest_custom)
+ should_email(@u_custom_global)
should_not_email(@unsubscriber)
should_not_email(@u_participating)
should_not_email(@u_disabled)
@@ -914,7 +1002,9 @@ describe NotificationService, services: true do
should_email(@u_watcher)
should_email(@u_participating)
should_email(@u_lazy_participant)
+ should_email(@u_custom_global)
should_not_email(@u_guest_watcher)
+ should_not_email(@u_guest_custom)
should_not_email(@u_disabled)
end
end
@@ -929,13 +1019,15 @@ describe NotificationService, services: true do
@u_committer = create(:user, username: 'committer')
@u_not_mentioned = create_global_setting_for(create(:user, username: 'regular'), :participating)
@u_outsider_mentioned = create(:user, username: 'outsider')
+ @u_custom_global = create_global_setting_for(create(:user, username: 'custom_global'), :custom)
# User to be participant by default
# This user does not contain any record in notification settings table
# It should be treated with a :participating notification_level
@u_lazy_participant = create(:user, username: 'lazy-participant')
- create_guest_watcher
+ @u_guest_watcher = create_user_with_notification(:watch, 'guest_watching')
+ @u_guest_custom = create_user_with_notification(:custom, 'guest_custom')
project.team << [@u_watcher, :master]
project.team << [@u_participating, :master]
@@ -945,6 +1037,7 @@ describe NotificationService, services: true do
project.team << [@u_committer, :master]
project.team << [@u_not_mentioned, :master]
project.team << [@u_lazy_participant, :master]
+ project.team << [@u_custom_global, :master]
end
def create_global_setting_for(user, level)
@@ -955,10 +1048,20 @@ describe NotificationService, services: true do
user
end
- def create_guest_watcher
- @u_guest_watcher = create(:user, username: 'guest_watching')
- setting = @u_guest_watcher.notification_settings_for(project)
- setting.level = :watch
+ def create_user_with_notification(level, username)
+ user = create(:user, username: username)
+ setting = user.notification_settings_for(project)
+ setting.level = level
+ setting.save
+
+ user
+ end
+
+ # Create custom notifications
+ # When resource is nil it means global notification
+ def update_custom_notification(event, user, resource = nil)
+ setting = user.notification_settings_for(resource)
+ setting.events[event] = true
setting.save
end
diff --git a/spec/workers/merge_worker_spec.rb b/spec/workers/merge_worker_spec.rb
index 1abd87d7d33..b5e1fdb8ded 100644
--- a/spec/workers/merge_worker_spec.rb
+++ b/spec/workers/merge_worker_spec.rb
@@ -9,7 +9,7 @@ describe MergeWorker do
before do
source_project.team << [author, :master]
- source_project.repository.expire_branch_names
+ source_project.repository.expire_branches_cache
end
it 'clears cache of source repo after removing source branch' do
diff --git a/vendor/gitignore/Android.gitignore b/vendor/gitignore/Android.gitignore
index a8368751267..f6b286cea98 100644
--- a/vendor/gitignore/Android.gitignore
+++ b/vendor/gitignore/Android.gitignore
@@ -2,7 +2,7 @@
*.apk
*.ap_
-# Files for the Dalvik VM
+# Files for the ART/Dalvik VM
*.dex
# Java class files
@@ -34,6 +34,7 @@ captures/
# Intellij
*.iml
+.idea/workspace.xml
# Keystore files
*.jks
diff --git a/vendor/gitignore/C++.gitignore b/vendor/gitignore/C++.gitignore
index b8bd0267bdf..4581ef2eeef 100644
--- a/vendor/gitignore/C++.gitignore
+++ b/vendor/gitignore/C++.gitignore
@@ -15,6 +15,7 @@
# Fortran module files
*.mod
+*.smod
# Compiled Static libraries
*.lai
diff --git a/vendor/gitignore/CMake.gitignore b/vendor/gitignore/CMake.gitignore
index b558e9afa6d..0cc7e4b5275 100644
--- a/vendor/gitignore/CMake.gitignore
+++ b/vendor/gitignore/CMake.gitignore
@@ -4,3 +4,4 @@ CMakeScripts
Makefile
cmake_install.cmake
install_manifest.txt
+CTestTestfile.cmake
diff --git a/vendor/gitignore/D.gitignore b/vendor/gitignore/D.gitignore
index b4433f8a512..74b926fc901 100644
--- a/vendor/gitignore/D.gitignore
+++ b/vendor/gitignore/D.gitignore
@@ -18,3 +18,7 @@
.dub
docs.json
__dummy.html
+docs/
+
+# Code coverage
+*.lst
diff --git a/vendor/gitignore/Global/Bazaar.gitignore b/vendor/gitignore/Global/Bazaar.gitignore
new file mode 100644
index 00000000000..3cbbcbd11ec
--- /dev/null
+++ b/vendor/gitignore/Global/Bazaar.gitignore
@@ -0,0 +1,2 @@
+.bzr/
+.bzrignore
diff --git a/vendor/gitignore/Global/OSX.gitignore b/vendor/gitignore/Global/OSX.gitignore
index 660b31353e8..5972fe50f66 100644
--- a/vendor/gitignore/Global/OSX.gitignore
+++ b/vendor/gitignore/Global/OSX.gitignore
@@ -1,4 +1,4 @@
-.DS_Store
+*.DS_Store
.AppleDouble
.LSOverride
@@ -15,6 +15,7 @@ Icon
.TemporaryItems
.Trashes
.VolumeIcon.icns
+.com.apple.timemachine.donotpresent
# Directories potentially created on remote AFP share
.AppleDB
diff --git a/vendor/gitignore/Global/README.md b/vendor/gitignore/Global/README.md
new file mode 100644
index 00000000000..06b6649bd9a
--- /dev/null
+++ b/vendor/gitignore/Global/README.md
@@ -0,0 +1,10 @@
+## Globally Useful gitignores
+
+This directory contains globally useful gitignores,
+e.g. OS-specific and editor specific.
+
+For more on global gitignores:
+<https://help.github.com/articles/ignoring-files/#create-a-global-gitignore>
+
+And a good blog post about 'em:
+<http://augustl.com/blog/2009/global_gitignores>
diff --git a/vendor/gitignore/Global/SublimeText.gitignore b/vendor/gitignore/Global/SublimeText.gitignore
index 1d4e6137591..69c8c2b29ce 100644
--- a/vendor/gitignore/Global/SublimeText.gitignore
+++ b/vendor/gitignore/Global/SublimeText.gitignore
@@ -12,3 +12,16 @@
# sftp configuration file
sftp-config.json
+
+# Package control specific files
+Package Control.last-run
+Package Control.ca-list
+Package Control.ca-bundle
+Package Control.system-ca-bundle
+Package Control.cache/
+Package Control.ca-certs/
+bh_unicode_properties.cache
+
+# Sublime-github package stores a github token in this file
+# https://packagecontrol.io/packages/sublime-github
+GitHub.sublime-settings
diff --git a/vendor/gitignore/Haskell.gitignore b/vendor/gitignore/Haskell.gitignore
index 096abdd90b3..a4ee41ab62b 100644
--- a/vendor/gitignore/Haskell.gitignore
+++ b/vendor/gitignore/Haskell.gitignore
@@ -16,3 +16,4 @@ cabal.sandbox.config
*.hp
*.eventlog
.stack-work/
+cabal.project.local
diff --git a/vendor/gitignore/Julia.gitignore b/vendor/gitignore/Julia.gitignore
new file mode 100644
index 00000000000..381e0b6d252
--- /dev/null
+++ b/vendor/gitignore/Julia.gitignore
@@ -0,0 +1,4 @@
+*.jl.cov
+*.jl.*.cov
+*.jl.mem
+deps/deps.jl
diff --git a/vendor/gitignore/LICENSE b/vendor/gitignore/LICENSE
new file mode 100644
index 00000000000..b8a103ac9b1
--- /dev/null
+++ b/vendor/gitignore/LICENSE
@@ -0,0 +1,19 @@
+Copyright (c) 2016 GitHub, Inc.
+
+Permission is hereby granted, free of charge, to any person obtaining a
+copy of this software and associated documentation files (the "Software"),
+to deal in the Software without restriction, including without limitation
+the rights to use, copy, modify, merge, publish, distribute, sublicense,
+and/or sell copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in
+all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
+DEALINGS IN THE SOFTWARE.
diff --git a/vendor/gitignore/Laravel.gitignore b/vendor/gitignore/Laravel.gitignore
index c491fa2bc6f..1cd717b6921 100644
--- a/vendor/gitignore/Laravel.gitignore
+++ b/vendor/gitignore/Laravel.gitignore
@@ -7,7 +7,6 @@ app/storage/
# Laravel 5 & Lumen specific
bootstrap/cache/
-storage/
.env.*.php
.env.php
.env
diff --git a/vendor/gitignore/Objective-C.gitignore b/vendor/gitignore/Objective-C.gitignore
index 3020bc327a7..86f21d8e0ff 100644
--- a/vendor/gitignore/Objective-C.gitignore
+++ b/vendor/gitignore/Objective-C.gitignore
@@ -24,6 +24,8 @@ xcuserdata/
## Obj-C/Swift specific
*.hmap
*.ipa
+*.dSYM.zip
+*.dSYM
# CocoaPods
#
@@ -49,3 +51,10 @@ Carthage/Build
fastlane/report.xml
fastlane/screenshots
+
+#Code Injection
+#
+# After new code Injection tools there's a generated folder /iOSInjectionProject
+# https://github.com/johnno1962/injectionforxcode
+
+iOSInjectionProject/
diff --git a/vendor/gitignore/Qt.gitignore b/vendor/gitignore/Qt.gitignore
index fa24b2efee8..c7659c24f38 100644
--- a/vendor/gitignore/Qt.gitignore
+++ b/vendor/gitignore/Qt.gitignore
@@ -34,5 +34,5 @@ Makefile*
*.qmlproject.user.*
# QtCtreator CMake
-CMakeLists.txt.user
+CMakeLists.txt.user*
diff --git a/vendor/gitignore/README.md b/vendor/gitignore/README.md
deleted file mode 100644
index 43131e815cc..00000000000
--- a/vendor/gitignore/README.md
+++ /dev/null
@@ -1,14 +0,0 @@
-# .gitignore templates
-
-This directory contains language-specific .gitignore templates that are used by GitLab.
-
-These files were automatically pulled from [this repository](https://github.com/github/gitignore).
-Please submit pull requests to that repository. There is no need to edit the files in this directory.
-
-## Bulk Update
-
-To update this directory with the latest changes in the repository, run:
-
-```sh
-bundle exec rake gitlab:update_gitignore
-```
diff --git a/vendor/gitignore/Rails.gitignore b/vendor/gitignore/Rails.gitignore
index 2121e0a8038..d8c256c1925 100644
--- a/vendor/gitignore/Rails.gitignore
+++ b/vendor/gitignore/Rails.gitignore
@@ -16,6 +16,10 @@ pickle-email-*.html
config/initializers/secret_token.rb
config/secrets.yml
+# dotenv
+# TODO Comment out this rule if environment variables can be committed
+.env
+
## Environment normalization:
/.bundle
/vendor/bundle
diff --git a/vendor/gitignore/Swift.gitignore b/vendor/gitignore/Swift.gitignore
index 8a29fa52af4..2c22487b5e3 100644
--- a/vendor/gitignore/Swift.gitignore
+++ b/vendor/gitignore/Swift.gitignore
@@ -24,6 +24,8 @@ xcuserdata/
## Obj-C/Swift specific
*.hmap
*.ipa
+*.dSYM.zip
+*.dSYM
## Playgrounds
timeline.xctimeline
diff --git a/vendor/gitignore/UnrealEngine.gitignore b/vendor/gitignore/UnrealEngine.gitignore
index 75b1186b0af..be0e4913c3a 100644
--- a/vendor/gitignore/UnrealEngine.gitignore
+++ b/vendor/gitignore/UnrealEngine.gitignore
@@ -37,6 +37,7 @@
*.suo
*.opensdf
*.sdf
+*.VC.db
*.VC.opendb
# Precompiled Assets
diff --git a/vendor/gitignore/VisualStudio.gitignore b/vendor/gitignore/VisualStudio.gitignore
index f1e3d20e056..67acbf42f5e 100644
--- a/vendor/gitignore/VisualStudio.gitignore
+++ b/vendor/gitignore/VisualStudio.gitignore
@@ -42,6 +42,7 @@ dlldata.c
# DNX
project.lock.json
+project.fragment.lock.json
artifacts/
*_i.c
diff --git a/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
new file mode 100644
index 00000000000..396d3f1b042
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Docker.gitlab-ci.yml
@@ -0,0 +1,7 @@
+# Official docker image.
+image: docker:latest
+
+build:
+ stage: build
+ script:
+ - docker build -t test .
diff --git a/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
new file mode 100644
index 00000000000..0b329aaf1c4
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Elixir.gitlab-ci.yml
@@ -0,0 +1,18 @@
+# This template uses the non default language docker image
+# The image already has Hex installed. You might want to consider to use `elixir:latest`
+image: trenpixster/elixir:latest
+
+# Pic zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+before_script:
+ - mix deps.get
+
+mix:
+ script:
+ - mix test
diff --git a/vendor/gitlab-ci-yml/LICENSE b/vendor/gitlab-ci-yml/LICENSE
new file mode 100644
index 00000000000..80f7b87b6c0
--- /dev/null
+++ b/vendor/gitlab-ci-yml/LICENSE
@@ -0,0 +1,21 @@
+The MIT License (MIT)
+
+Copyright (c) 2016 GitLab.org
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml
new file mode 100644
index 00000000000..e5bce3503f3
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Nodejs.gitlab-ci.yml
@@ -0,0 +1,27 @@
+# Official framework image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/node/tags/
+image: node:latest
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+# This folder is cached between builds
+# http://docs.gitlab.com/ce/ci/yaml/README.html#cache
+cache:
+ paths:
+ - node_modules/
+
+test_async:
+ script:
+ - npm install
+ - node ./specs/start.js ./specs/async.spec.js
+
+test_db:
+ script:
+ - npm install
+ - node ./specs/start.js ./specs/db-postgres.spec.js
diff --git a/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml
new file mode 100644
index 00000000000..7fcc0b436b5
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/brunch.gitlab-ci.yml
@@ -0,0 +1,16 @@
+# Full project: https://gitlab.com/pages/brunch
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules/
+
+ script:
+ - npm install -g brunch
+ - brunch build --production
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml
new file mode 100644
index 00000000000..791afdd23f1
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/doxygen.gitlab-ci.yml
@@ -0,0 +1,13 @@
+# Full project: https://gitlab.com/pages/doxygen
+image: alpine
+
+pages:
+ script:
+ - apk update && apk add doxygen
+ - doxygen doxygen/Doxyfile
+ - mv doxygen/documentation/html/ public/
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml
new file mode 100644
index 00000000000..dd3ef149668
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/harp.gitlab-ci.yml
@@ -0,0 +1,16 @@
+# Full project: https://gitlab.com/pages/harp
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules
+
+ script:
+ - npm install -g harp
+ - harp compile ./ public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml
new file mode 100644
index 00000000000..b468d79bcad
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/hexo.gitlab-ci.yml
@@ -0,0 +1,25 @@
+# Full project: https://gitlab.com/pages/hexo
+image: python:2.7
+
+cache:
+ paths:
+ - vendor/
+
+test:
+ stage: test
+ script:
+ - pip install hyde
+ - hyde gen
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - pip install hyde
+ - hyde gen -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml
new file mode 100644
index 00000000000..249a168aa33
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/html.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/plain-html
+pages:
+ stage: deploy
+ script:
+ - mkdir .public
+ - cp -r * .public
+ - mv .public public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml
new file mode 100644
index 00000000000..45df6975259
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/hugo.gitlab-ci.yml
@@ -0,0 +1,11 @@
+# Full project: https://gitlab.com/pages/hugo
+image: publysher/hugo
+
+pages:
+ script:
+ - hugo
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml
new file mode 100644
index 00000000000..f5b40f2b9f1
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/hyde.gitlab-ci.yml
@@ -0,0 +1,25 @@
+# Full project: https://gitlab.com/pages/hyde
+image: python:2.7
+
+cache:
+ paths:
+ - vendor/
+
+test:
+ stage: test
+ script:
+ - pip install hyde
+ - hyde gen
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - pip install hyde
+ - hyde gen -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml
new file mode 100644
index 00000000000..36918fc005a
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/jekyll.gitlab-ci.yml
@@ -0,0 +1,24 @@
+# Full project: https://gitlab.com/pages/jekyll
+image: ruby:2.3
+
+test:
+ stage: test
+ script:
+ - gem install jekyll
+ - jekyll build -d test
+ artifacts:
+ paths:
+ - test
+ except:
+ - master
+
+pages:
+ stage: deploy
+ script:
+ - gem install jekyll
+ - jekyll build -d public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml
new file mode 100644
index 00000000000..c5c44a5d86c
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/lektor.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/hyde
+image: python:2.7
+
+pages:
+ script:
+ - pip install lektor
+ - lektor build --output-path public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml
new file mode 100644
index 00000000000..50e8b7ccd46
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/metalsmith.gitlab-ci.yml
@@ -0,0 +1,17 @@
+# Full project: https://gitlab.com/pages/metalsmith
+image: node:4.2.2
+
+pages:
+ cache:
+ paths:
+ - node_modules/
+
+ script:
+ - npm install -g metalsmith
+ - npm install
+ - make build
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml
new file mode 100644
index 00000000000..9f4cc0574d6
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/middleman.gitlab-ci.yml
@@ -0,0 +1,27 @@
+# Full project: https://gitlab.com/pages/middleman
+image: ruby:2.3
+
+cache:
+ paths:
+ - vendor
+
+test:
+ script:
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
+ except:
+ - master
+
+pages:
+ script:
+ - apt-get update -yqqq
+ - apt-get install -y nodejs
+ - bundle install --path vendor
+ - bundle exec middleman build
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml
new file mode 100644
index 00000000000..b469b316ba5
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/nanoc.gitlab-ci.yml
@@ -0,0 +1,12 @@
+# Full project: https://gitlab.com/pages/nanoc
+image: ruby:2.3
+
+pages:
+ script:
+ - bundle install -j4
+ - nanoc
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml
new file mode 100644
index 00000000000..4762ec9acfd
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/octopress.gitlab-ci.yml
@@ -0,0 +1,15 @@
+# Full project: https://gitlab.com/pages/octopress
+image: ruby:2.3
+
+pages:
+ script:
+ - apt-get update -qq && apt-get install -qq nodejs
+ - bundle install -j4
+ - bundle exec rake generate
+ - mv public .public
+ - mv .public/octopress public
+ artifacts:
+ paths:
+ - public
+ only:
+ - master
diff --git a/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml b/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml
new file mode 100644
index 00000000000..c5f3154f587
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Pages/pelican.gitlab-ci.yml
@@ -0,0 +1,10 @@
+# Full project: https://gitlab.com/pages/pelican
+image: python:2.7-alpine
+
+pages:
+ script:
+ - pip install -r requirements.txt
+ - pelican -s publishconf.py
+ artifacts:
+ paths:
+ - public/
diff --git a/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
new file mode 100644
index 00000000000..78f3e39949f
--- /dev/null
+++ b/vendor/gitlab-ci-yml/Ruby.gitlab-ci.yml
@@ -0,0 +1,30 @@
+# Official language image. Look for the different tagged releases at:
+# https://hub.docker.com/r/library/ruby/tags/
+image: "ruby:2.3"
+
+# Pick zero or more services to be used on all builds.
+# Only needed when using a docker container to run your tests in.
+# Check out: http://docs.gitlab.com/ce/ci/docker/using_docker_images.html#what-is-service
+services:
+ - mysql:latest
+ - redis:latest
+ - postgres:latest
+
+# This is a basic example for a gem or script which doesn't use
+# services such as redis or postgres
+before_script:
+ - gem install bundler # Bundler is not installed with the image
+ - bundle install -j $(nproc) # Install dependencies
+
+rubocop:
+ script:
+ - rubocop
+
+rspec:
+ script:
+ - rspec spec
+
+rails:
+ script:
+ - rake db:migrate
+ - rspec spec