summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-07-26 14:54:45 +0200
committerGrzegorz Bizon <grzesiek.bizon@gmail.com>2017-07-26 14:54:45 +0200
commit79a7f7b6e59fa1225c440547796331caedabeaab (patch)
treef002dd7d26c38f358e626e269543628e06c4cb4b
parent7d6538f2e2a6f1b0808a77e347a9083295b17c8c (diff)
parent5f35901a01ff822ba1e637251d9726a41e73ed17 (diff)
downloadgitlab-ce-79a7f7b6e59fa1225c440547796331caedabeaab.tar.gz
Merge branch 'master' into backstage/gb/migrate-stages-statuses
* master: (110 commits) Add missing colon Fix project wiki web_url spec Resolve "Memory usage notice doesn't link anywhere" Docs new topic "user/index" Implement GRPC call to RepositoryService Pending delete projects should not show in deploy keys Remove outdated ~Frontend label in CONTRIBUTING.md Fixes 500 error caused by pending delete projects in admin dashboard Add lower path index to redirect_routes Remove project_key from the Jira configuration Update CHANGELOG.md for 9.4.1 Enable gitaly_post_upload_pack by default Add `api` prefix as a top level route in the spec. Move relative_path to the element that is being clicked Bumps Gitlab Omniauth LDAP version Add directives to Vue component ordering synchronize ukrainian translation in zanata again v3 API is unsupported after 9.5, but may not be removed Fix vertical alignment in firefox and safari for pipeline mini graph Adds link_to_gfm method instrumentation ... Conflicts: db/schema.rb
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG.md12
-rw-r--r--CONTRIBUTING.md32
-rw-r--r--Gemfile7
-rw-r--r--Gemfile.lock33
-rw-r--r--app/assets/javascripts/copy_as_gfm.js10
-rw-r--r--app/assets/javascripts/how_to_merge.js12
-rw-r--r--app/assets/javascripts/lazy_loader.js76
-rw-r--r--app/assets/javascripts/main.js14
-rw-r--r--app/assets/javascripts/merge_conflicts/merge_conflict_store.js2
-rw-r--r--app/assets/javascripts/star.js6
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js3
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js11
-rw-r--r--app/assets/stylesheets/framework/avatar.scss2
-rw-r--r--app/assets/stylesheets/framework/calendar.scss2
-rw-r--r--app/assets/stylesheets/framework/files.scss10
-rw-r--r--app/assets/stylesheets/framework/header.scss29
-rw-r--r--app/assets/stylesheets/framework/nav.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss11
-rw-r--r--app/assets/stylesheets/framework/variables.scss4
-rw-r--r--app/assets/stylesheets/new_nav.scss5
-rw-r--r--app/assets/stylesheets/pages/boards.scss5
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss3
-rw-r--r--app/controllers/admin/application_settings_controller.rb4
-rw-r--r--app/controllers/admin/dashboard_controller.rb2
-rw-r--r--app/controllers/admin/projects_controller.rb15
-rw-r--r--app/controllers/projects/application_controller.rb1
-rw-r--r--app/controllers/projects/issues_controller.rb9
-rw-r--r--app/controllers/projects/merge_requests_controller.rb6
-rw-r--r--app/controllers/projects_controller.rb4
-rw-r--r--app/controllers/sessions_controller.rb8
-rw-r--r--app/finders/admin/projects_finder.rb33
-rw-r--r--app/helpers/avatars_helper.rb9
-rw-r--r--app/helpers/emails_helper.rb4
-rw-r--r--app/helpers/issues_helper.rb26
-rw-r--r--app/helpers/lazy_image_tag_helper.rb24
-rw-r--r--app/helpers/system_note_helper.rb3
-rw-r--r--app/helpers/version_check_helper.rb2
-rw-r--r--app/models/ci/pipeline.rb2
-rw-r--r--app/models/ci/pipeline_schedule.rb4
-rw-r--r--app/models/concerns/cache_markdown_field.rb2
-rw-r--r--app/models/concerns/protected_ref.rb17
-rw-r--r--app/models/group.rb6
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/project.rb10
-rw-r--r--app/models/project_services/issue_tracker_service.rb8
-rw-r--r--app/models/project_services/jira_service.rb20
-rw-r--r--app/models/repository.rb13
-rw-r--r--app/models/system_note_metadata.rb3
-rw-r--r--app/policies/ci/build_policy.rb8
-rw-r--r--app/policies/ci/pipeline_policy.rb12
-rw-r--r--app/policies/global_policy.rb2
-rw-r--r--app/policies/project_policy.rb6
-rw-r--r--app/serializers/build_details_entity.rb3
-rw-r--r--app/serializers/deploy_key_entity.rb2
-rw-r--r--app/services/ci/create_pipeline_service.rb73
-rw-r--r--app/services/ci/create_trigger_request_service.rb8
-rw-r--r--app/services/issuable_base_service.rb1
-rw-r--r--app/services/issues/base_service.rb8
-rw-r--r--app/services/issues/close_service.rb4
-rw-r--r--app/services/issues/duplicate_service.rb24
-rw-r--r--app/services/issues/update_service.rb18
-rw-r--r--app/services/projects/update_service.rb9
-rw-r--r--app/services/quick_actions/interpret_service.rb18
-rw-r--r--app/services/system_note_service.rb38
-rw-r--r--app/views/admin/dashboard/index.html.haml2
-rw-r--r--app/views/layouts/header/_new.html.haml2
-rw-r--r--app/views/layouts/nav/_new_project_sidebar.html.haml6
-rw-r--r--app/views/layouts/nav/_project.html.haml6
-rw-r--r--app/views/projects/blame/show.html.haml4
-rw-r--r--app/views/projects/blob/viewers/_image.html.haml2
-rw-r--r--app/views/projects/buttons/_star.html.haml2
-rw-r--r--app/views/projects/diffs/viewers/_image.html.haml14
-rw-r--r--app/views/projects/merge_requests/_how_to_merge.html.haml14
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/milestones/index.html.haml6
-rw-r--r--app/views/shared/_mr_head.html.haml2
-rw-r--r--app/views/shared/_new_project_item_select.html.haml4
-rw-r--r--app/views/shared/icons/_icon_clone.svg3
-rw-r--r--app/workers/pipeline_schedule_worker.rb13
-rw-r--r--changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml4
-rw-r--r--changelogs/unreleased/26372-duplicate-issue-slash-command.yml4
-rw-r--r--changelogs/unreleased/28202_decrease_abc_threshold_step2.yml4
-rw-r--r--changelogs/unreleased/2971-multiproject-grah-ce-port.yml4
-rw-r--r--changelogs/unreleased/30634-protected-pipeline.yml5
-rw-r--r--changelogs/unreleased/31129-jira-project-key-elim.yml4
-rw-r--r--changelogs/unreleased/31533-usage-data-projects-stats.yml4
-rw-r--r--changelogs/unreleased/33097-issue-tracker.yml4
-rw-r--r--changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml4
-rw-r--r--changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml4
-rw-r--r--changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml4
-rw-r--r--changelogs/unreleased/35204-doc-api-ci-lint-typo.yml4
-rw-r--r--changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml4
-rw-r--r--changelogs/unreleased/35391-fix-star-i18n-in-js.yml4
-rw-r--r--changelogs/unreleased/35399-mini-graph-commits-box.yml4
-rw-r--r--changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml4
-rw-r--r--changelogs/unreleased/35478-allow-admin-to-read-user-list.yml4
-rw-r--r--changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml4
-rw-r--r--changelogs/unreleased/bump-omniauth-ldap-gem-version.yml4
-rw-r--r--changelogs/unreleased/bvl-free-unused-names.yml5
-rw-r--r--changelogs/unreleased/dz-fix-calendar-today.yml4
-rw-r--r--changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml4
-rw-r--r--changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml4
-rw-r--r--changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml4
-rw-r--r--changelogs/unreleased/new-navigation-custom-logo.yml4
-rw-r--r--changelogs/unreleased/post-upload-pack-opt-out.yml4
-rw-r--r--config/initializers/8_metrics.rb3
-rw-r--r--config/initializers/grape_route_helpers_fix.rb35
-rw-r--r--config/routes/api.rb2
-rw-r--r--config/webpack.config.js1
-rw-r--r--db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb34
-rw-r--r--db/schema.rb2
-rw-r--r--doc/README.md7
-rw-r--r--doc/administration/container_registry.md29
-rw-r--r--doc/administration/high_availability/database.md7
-rw-r--r--doc/api/README.md3
-rw-r--r--doc/api/ci/lint.md2
-rw-r--r--doc/api/group_milestones.md120
-rw-r--r--doc/api/issues.md40
-rw-r--r--doc/api/projects.md91
-rw-r--r--doc/api/v3_to_v4.md8
-rw-r--r--doc/articles/index.md21
-rw-r--r--doc/development/code_review.md49
-rw-r--r--doc/development/fe_guide/performance.md12
-rw-r--r--doc/development/fe_guide/style_guide_js.md1
-rw-r--r--doc/integration/README.md20
-rw-r--r--doc/integration/external-issue-tracker.md6
-rw-r--r--doc/user/index.md175
-rw-r--r--doc/user/project/integrations/bugzilla.md4
-rw-r--r--doc/user/project/integrations/jira.md4
-rw-r--r--doc/user/project/integrations/redmine.md2
-rw-r--r--doc/user/project/pipelines/schedules.md7
-rw-r--r--doc/user/project/quick_actions.md1
-rw-r--r--features/steps/project/services.rb1
-rw-r--r--features/steps/project/wiki.rb2
-rw-r--r--lib/api/api.rb4
-rw-r--r--lib/api/entities.rb58
-rw-r--r--lib/api/group_milestones.rb85
-rw-r--r--lib/api/helpers.rb4
-rw-r--r--lib/api/helpers/related_resources_helpers.rb28
-rw-r--r--lib/api/issues.rb2
-rw-r--r--lib/api/merge_requests.rb17
-rw-r--r--lib/api/milestone_responses.rb98
-rw-r--r--lib/api/milestones.rb154
-rw-r--r--lib/api/project_milestones.rb91
-rw-r--r--lib/api/services.rb6
-rw-r--r--lib/api/triggers.rb11
-rw-r--r--lib/api/v3/entities.rb31
-rw-r--r--lib/api/v3/triggers.rb11
-rw-r--r--lib/banzai/filter/gollum_tags_filter.rb2
-rw-r--r--lib/banzai/filter/image_lazy_load_filter.rb16
-rw-r--r--lib/banzai/filter/image_link_filter.rb2
-rw-r--r--lib/banzai/filter/issue_reference_filter.rb2
-rw-r--r--lib/banzai/filter/relative_link_filter.rb1
-rw-r--r--lib/banzai/pipeline/gfm_pipeline.rb1
-rw-r--r--lib/banzai/reference_parser/external_issue_parser.rb10
-rw-r--r--lib/ci/api/triggers.rb11
-rw-r--r--lib/gitlab/git/repository.rb10
-rw-r--r--lib/gitlab/gitaly_client.rb2
-rw-r--r--lib/gitlab/gitaly_client/repository_service.rb16
-rw-r--r--lib/gitlab/path_regex.rb20
-rw-r--r--lib/gitlab/reference_extractor.rb7
-rw-r--r--lib/gitlab/slash_commands/issue_command.rb2
-rw-r--r--lib/gitlab/untrusted_regexp.rb30
-rw-r--r--lib/gitlab/usage_data.rb15
-rw-r--r--lib/gitlab/user_access.rb36
-rw-r--r--lib/gitlab/workhorse.rb7
-rw-r--r--lib/tasks/migrate/setup_postgresql.rake2
-rw-r--r--locale/ja/gitlab.po7
-rw-r--r--locale/pt_BR/gitlab.po27
-rw-r--r--locale/uk/gitlab.po108
-rw-r--r--locale/zh_CN/gitlab.po10
-rw-r--r--locale/zh_HK/gitlab.po8
-rw-r--r--locale/zh_TW/gitlab.po4
-rw-r--r--spec/controllers/admin/dashboard_controller_spec.rb21
-rw-r--r--spec/controllers/projects/issues_controller_spec.rb61
-rw-r--r--spec/controllers/projects/jobs_controller_spec.rb4
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/pipelines_controller_spec.rb7
-rw-r--r--spec/controllers/sent_notifications_controller_spec.rb6
-rw-r--r--spec/features/admin/admin_appearance_spec.rb4
-rw-r--r--spec/features/dashboard/issues_spec.rb18
-rw-r--r--spec/features/explore/new_menu_spec.rb17
-rw-r--r--spec/features/issuables/markdown_references_spec.rb193
-rw-r--r--spec/features/issues/user_uses_slash_commands_spec.rb37
-rw-r--r--spec/features/markdown_spec.rb2
-rw-r--r--spec/features/projects/features_visibility_spec.rb19
-rw-r--r--spec/features/projects/services/jira_service_spec.rb21
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_group_spec.rb2
-rw-r--r--spec/features/uploads/user_uploads_avatar_to_profile_spec.rb2
-rw-r--r--spec/finders/admin/projects_finder_spec.rb136
-rw-r--r--spec/helpers/application_helper_spec.rb7
-rw-r--r--spec/helpers/avatars_helper_spec.rb48
-rw-r--r--spec/helpers/issues_helper_spec.rb8
-rw-r--r--spec/javascripts/lazy_loader_spec.js57
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js1
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js2
-rw-r--r--spec/lib/banzai/filter/external_issue_reference_filter_spec.rb5
-rw-r--r--spec/lib/banzai/filter/gollum_tags_filter_spec.rb4
-rw-r--r--spec/lib/banzai/filter/image_lazy_load_filter_spec.rb19
-rw-r--r--spec/lib/banzai/pipeline/gfm_pipeline_spec.rb89
-rw-r--r--spec/lib/gitlab/ci/status/build/cancelable_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/build/factory_spec.rb25
-rw-r--r--spec/lib/gitlab/ci/status/build/retryable_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/status/build/stop_spec.rb4
-rw-r--r--spec/lib/gitlab/ci/trace/stream_spec.rb14
-rw-r--r--spec/lib/gitlab/gitaly_client/repository_service_spec.rb19
-rw-r--r--spec/lib/gitlab/path_regex_spec.rb61
-rw-r--r--spec/lib/gitlab/reference_extractor_spec.rb31
-rw-r--r--spec/lib/gitlab/untrusted_regexp_spec.rb8
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb29
-rw-r--r--spec/lib/gitlab/workhorse_spec.rb7
-rw-r--r--spec/models/ability_spec.rb8
-rw-r--r--spec/models/ci/pipeline_spec.rb4
-rw-r--r--spec/models/concerns/mentionable_spec.rb18
-rw-r--r--spec/models/group_spec.rb4
-rw-r--r--spec/models/list_spec.rb6
-rw-r--r--spec/models/merge_request_spec.rb52
-rw-r--r--spec/models/notification_setting_spec.rb9
-rw-r--r--spec/models/pages_domain_spec.rb2
-rw-r--r--spec/models/project_services/jira_service_spec.rb10
-rw-r--r--spec/models/project_spec.rb45
-rw-r--r--spec/models/project_wiki_spec.rb2
-rw-r--r--spec/models/redirect_route_spec.rb2
-rw-r--r--spec/models/repository_spec.rb16
-rw-r--r--spec/models/route_spec.rb2
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/policies/ci/build_policy_spec.rb76
-rw-r--r--spec/policies/ci/pipeline_policy_spec.rb66
-rw-r--r--spec/policies/global_policy_spec.rb20
-rw-r--r--spec/policies/project_policy_spec.rb42
-rw-r--r--spec/requests/api/group_milestones_spec.rb21
-rw-r--r--spec/requests/api/issues_spec.rb13
-rw-r--r--spec/requests/api/merge_requests_spec.rb14
-rw-r--r--spec/requests/api/project_milestones_spec.rb25
-rw-r--r--spec/requests/api/projects_spec.rb57
-rw-r--r--spec/requests/api/triggers_spec.rb3
-rw-r--r--spec/requests/api/users_spec.rb19
-rw-r--r--spec/requests/api/v3/triggers_spec.rb3
-rw-r--r--spec/requests/ci/api/triggers_spec.rb14
-rw-r--r--spec/serializers/build_details_entity_spec.rb89
-rw-r--r--spec/serializers/deploy_key_entity_spec.rb4
-rw-r--r--spec/serializers/job_entity_spec.rb11
-rw-r--r--spec/serializers/pipeline_details_entity_spec.rb6
-rw-r--r--spec/serializers/pipeline_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb33
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb215
-rw-r--r--spec/services/ci/create_trigger_request_service_spec.rb24
-rw-r--r--spec/services/ci/process_pipeline_service_spec.rb2
-rw-r--r--spec/services/ci/retry_build_service_spec.rb4
-rw-r--r--spec/services/ci/retry_pipeline_service_spec.rb20
-rw-r--r--spec/services/create_deployment_service_spec.rb2
-rw-r--r--spec/services/git_push_service_spec.rb58
-rw-r--r--spec/services/issues/close_service_spec.rb8
-rw-r--r--spec/services/issues/duplicate_service_spec.rb80
-rw-r--r--spec/services/issues/update_service_spec.rb21
-rw-r--r--spec/services/merge_requests/build_service_spec.rb2
-rw-r--r--spec/services/projects/update_service_spec.rb9
-rw-r--r--spec/services/quick_actions/interpret_service_spec.rb49
-rw-r--r--spec/services/system_note_service_spec.rb50
-rw-r--r--spec/spec_helper.rb7
-rw-r--r--spec/support/api/milestones_shared_examples.rb (renamed from spec/requests/api/milestones_spec.rb)190
-rw-r--r--spec/support/features/issuable_slash_commands_shared_examples.rb19
-rw-r--r--spec/support/jira_service_helper.rb2
-rw-r--r--spec/support/matchers/markdown_matchers.rb4
-rw-r--r--spec/support/stub_configuration.rb5
-rw-r--r--spec/workers/post_receive_spec.rb1
267 files changed, 4051 insertions, 1024 deletions
diff --git a/.rubocop.yml b/.rubocop.yml
index 9785e7626f9..f661a29d9d1 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -563,7 +563,7 @@ Style/Proc:
# branches, and conditions.
Metrics/AbcSize:
Enabled: true
- Max: 57.08
+ Max: 56.96
# This cop checks if the length of a block exceeds some maximum value.
Metrics/BlockLength:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index daf154eeb07..580d2357512 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,18 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.4.1 (2017-07-25)
+
+- Fix pipeline_schedules pages throwing error 500 (when ref is empty). !12983
+- Fix editing project with container images present. !13028
+- Fix some invalid entries in PO files. !13032
+- Fix cross site request protection when logging in as a regular user when LDAP is enabled. !13049
+- Fix bug causing metrics files to be truncated. !35420
+- Fix anonymous access to public projects in groups with pending invites.
+- Fixed issue boards sidebar close icon size.
+- Fixed duplicate new milestone buttons when new navigation is turned on.
+- Fix margins in the mini graph for pipeline in commits box.
+
## 9.4.0 (2017-07-22)
- Add blame view age mapping. !7198 (Jeff Stubler)
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 89e505709a3..12fb34b24be 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -31,7 +31,7 @@ _This notice should stay as the first item in the CONTRIBUTING.MD file._
- [Issue tracker guidelines](#issue-tracker-guidelines)
- [Issue weight](#issue-weight)
- [Regression issues](#regression-issues)
- - [Technical debt](#technical-debt)
+ - [Technical and UX debt](#technical-and-ux-debt)
- [Stewardship](#stewardship)
- [Merge requests](#merge-requests)
- [Merge request guidelines](#merge-request-guidelines)
@@ -114,8 +114,8 @@ scheduling into milestones. Labelling is a task for everyone.
Most issues will have labels for at least one of the following:
- Type: ~"feature proposal", ~bug, ~customer, etc.
-- Subject: ~wiki, ~"container registry", ~ldap, ~api, etc.
-- Team: ~CI, ~Discussion, ~Edge, ~Frontend, ~Platform, etc.
+- Subject: ~wiki, ~"container registry", ~ldap, ~api, ~frontend, etc.
+- Team: ~CI, ~Discussion, ~Edge, ~Platform, etc.
- Priority: ~Deliverable, ~Stretch
All labels, their meaning and priority are defined on the
@@ -278,7 +278,7 @@ For feature proposals for EE, open an issue on the
In order to help track the feature proposals, we have created a
[`feature proposal`][fpl] label. For the time being, users that are not members
of the project cannot add labels. You can instead ask one of the [core team]
-members to add the label `feature proposal` to the issue or add the following
+members to add the label ~"feature proposal" to the issue or add the following
code snippet right after your description in a new line: `~"feature proposal"`.
Please keep feature proposals as small and simple as possible, complex ones
@@ -344,27 +344,29 @@ addressed.
[8.3 Regressions]: https://gitlab.com/gitlab-org/gitlab-ce/issues/4127
[update the notes]: https://gitlab.com/gitlab-org/release-tools/blob/master/doc/pro-tips.md#update-the-regression-issue
-### Technical debt
+### Technical and UX debt
-In order to track things that can be improved in GitLab's codebase, we created
-the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
+In order to track things that can be improved in GitLab's codebase,
+we use the ~"technical debt" label in [GitLab's issue tracker][ce-tracker].
+For user experience improvements, we use the ~"UX debt" label.
-This label should be added to issues that describe things that can be improved,
-shortcuts that have been taken, code that needs refactoring, features that need
-additional attention, and all other things that have been left behind due to
-high velocity of development.
+These labels should be added to issues that describe things that can be improved,
+shortcuts that have been taken, features that need additional attention, and all
+other things that have been left behind due to high velocity of development.
+For example, code that needs refactoring should use the ~"technical debt" label,
+user experience refinements should use the ~"UX debt" label.
Everyone can create an issue, though you may need to ask for adding a specific
label, if you do not have permissions to do it by yourself. Additional labels
-can be combined with the `technical debt` label, to make it easier to schedule
+can be combined with these labels, to make it easier to schedule
the improvements for a release.
-Issues tagged with the `technical debt` label have the same priority like issues
+Issues tagged with these labels have the same priority like issues
that describe a new feature to be introduced in GitLab, and should be scheduled
for a release by the appropriate person.
-Make sure to mention the merge request that the `technical debt` issue is
-associated with in the description of the issue.
+Make sure to mention the merge request that the ~"technical debt" issue or
+~"UX debt" issue is associated with in the description of the issue.
### Stewardship
diff --git a/Gemfile b/Gemfile
index 1ee44680774..5bc8569a9e0 100644
--- a/Gemfile
+++ b/Gemfile
@@ -16,6 +16,7 @@ gem 'mysql2', '~> 0.4.5', group: :mysql
gem 'pg', '~> 0.18.2', group: :postgres
gem 'rugged', '~> 0.25.1.1'
+gem 'grape-route-helpers', '~> 2.0.0'
gem 'faraday', '~> 0.12'
@@ -60,7 +61,7 @@ gem 'browser', '~> 2.2'
# LDAP Auth
# GitLab fork with several improvements to original library. For full list of changes
# see https://github.com/intridea/omniauth-ldap/compare/master...gitlabhq:master
-gem 'gitlab_omniauth-ldap', '~> 1.2.1', require: 'omniauth-ldap'
+gem 'gitlab_omniauth-ldap', '~> 2.0.3', require: 'omniauth-ldap'
# Git Wiki
# Required manually in config/initializers/gollum.rb to control load order
@@ -164,7 +165,7 @@ gem 'rainbow', '~> 2.2'
gem 'settingslogic', '~> 2.0.9'
# Linear-time regex library for untrusted regular expressions
-gem 're2', '~> 1.0.0'
+gem 're2', '~> 1.1.1'
# Misc
@@ -353,7 +354,7 @@ group :development, :test do
end
group :test do
- gem 'shoulda-matchers', '~> 2.8.0', require: false
+ gem 'shoulda-matchers', '~> 3.1.2', require: false
gem 'email_spec', '~> 1.6.0'
gem 'json-schema', '~> 2.6.2'
gem 'webmock', '~> 2.3.2'
diff --git a/Gemfile.lock b/Gemfile.lock
index b0b437ae342..58a203c9b61 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -288,11 +288,11 @@ GEM
mime-types (>= 1.16, < 3)
posix-spawn (~> 0.3)
gitlab-markup (1.5.1)
- gitlab_omniauth-ldap (1.2.1)
- net-ldap (~> 0.9)
- omniauth (~> 1.0)
- pyu-ruby-sasl (~> 0.0.3.1)
- rubyntlm (~> 0.3)
+ gitlab_omniauth-ldap (2.0.3)
+ net-ldap (~> 0.16)
+ omniauth (~> 1.3)
+ pyu-ruby-sasl (>= 0.0.3.3, < 0.1)
+ rubyntlm (~> 0.5)
globalid (0.3.7)
activesupport (>= 4.1.0)
gollum-grit_adapter (1.0.1)
@@ -345,6 +345,10 @@ GEM
grape-entity (0.6.0)
activesupport
multi_json (>= 1.3.2)
+ grape-route-helpers (2.0.0)
+ activesupport
+ grape (~> 0.16, >= 0.16.0)
+ rake
grpc (1.4.0)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
@@ -467,7 +471,7 @@ GEM
mustermann-grape (1.0.0)
mustermann (~> 1.0.0)
mysql2 (0.4.5)
- net-ldap (0.12.1)
+ net-ldap (0.16.0)
netrc (0.11.0)
nokogiri (1.6.8.1)
mini_portile2 (~> 2.1.0)
@@ -591,7 +595,7 @@ GEM
premailer-rails (1.9.7)
actionmailer (>= 3, < 6)
premailer (~> 1.7, >= 1.7.9)
- prometheus-client-mmap (0.7.0.beta9)
+ prometheus-client-mmap (0.7.0.beta10)
mmap2 (~> 2.2, >= 2.2.7)
pry (0.10.4)
coderay (~> 1.1.0)
@@ -656,7 +660,7 @@ GEM
debugger-ruby_core_source (~> 1.3)
rdoc (4.2.2)
json (~> 1.4)
- re2 (1.0.0)
+ re2 (1.1.1)
recaptcha (3.0.0)
json
recursive-open-struct (1.0.0)
@@ -740,7 +744,7 @@ GEM
nokogiri (>= 1.5.10)
ruby_parser (3.9.0)
sexp_processor (~> 4.1)
- rubyntlm (0.5.2)
+ rubyntlm (0.6.2)
rubypants (0.2.0)
rubyzip (1.2.1)
rufus-scheduler (3.4.0)
@@ -774,8 +778,8 @@ GEM
sexp_processor (4.9.0)
sham_rack (1.3.6)
rack
- shoulda-matchers (2.8.0)
- activesupport (>= 3.0.0)
+ shoulda-matchers (3.1.2)
+ activesupport (>= 4.0.0)
sidekiq (5.0.4)
concurrent-ruby (~> 1.0)
connection_pool (~> 2.2, >= 2.2.0)
@@ -974,13 +978,14 @@ DEPENDENCIES
github-linguist (~> 4.7.0)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-markup (~> 1.5.1)
- gitlab_omniauth-ldap (~> 1.2.1)
+ gitlab_omniauth-ldap (~> 2.0.3)
gollum-lib (~> 4.2)
gollum-rugged_adapter (~> 0.4.4)
gon (~> 6.1.0)
google-api-client (~> 0.8.6)
grape (~> 0.19.2)
grape-entity (~> 0.6.0)
+ grape-route-helpers (~> 2.0.0)
haml_lint (~> 0.21.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
@@ -1055,7 +1060,7 @@ DEPENDENCIES
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rdoc (~> 4.2)
- re2 (~> 1.0.0)
+ re2 (~> 1.1.1)
recaptcha (~> 3.0)
redcarpet (~> 3.4)
redis (~> 3.2)
@@ -1084,7 +1089,7 @@ DEPENDENCIES
sentry-raven (~> 2.5.3)
settingslogic (~> 2.0.9)
sham_rack (~> 1.3.6)
- shoulda-matchers (~> 2.8.0)
+ shoulda-matchers (~> 3.1.2)
sidekiq (~> 5.0)
sidekiq-cron (~> 0.6.0)
sidekiq-limit_fetch (~> 3.4)
diff --git a/app/assets/javascripts/copy_as_gfm.js b/app/assets/javascripts/copy_as_gfm.js
index ba9d9a3e1f7..54257531284 100644
--- a/app/assets/javascripts/copy_as_gfm.js
+++ b/app/assets/javascripts/copy_as_gfm.js
@@ -1,6 +1,7 @@
/* eslint-disable class-methods-use-this, object-shorthand, no-unused-vars, no-use-before-define, no-new, max-len, no-restricted-syntax, guard-for-in, no-continue */
import './lib/utils/common_utils';
+import { placeholderImage } from './lazy_loader';
const gfmRules = {
// The filters referenced in lib/banzai/pipeline/gfm_pipeline.rb convert
@@ -56,6 +57,11 @@ const gfmRules = {
return text;
},
},
+ ImageLazyLoadFilter: {
+ 'img'(el, text) {
+ return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ },
+ },
VideoLinkFilter: {
'.video-container'(el) {
const videoEl = el.querySelector('video');
@@ -163,7 +169,9 @@ const gfmRules = {
return text.trim().split('\n').map(s => `> ${s}`.trim()).join('\n');
},
'img'(el) {
- return `![${el.getAttribute('alt')}](${el.getAttribute('src')})`;
+ const imageSrc = el.src;
+ const imageUrl = imageSrc && imageSrc !== placeholderImage ? imageSrc : (el.dataset.src || '');
+ return `![${el.getAttribute('alt')}](${imageUrl})`;
},
'a.anchor'(el, text) {
// Don't render a Markdown link for the anchor link inside a heading
diff --git a/app/assets/javascripts/how_to_merge.js b/app/assets/javascripts/how_to_merge.js
new file mode 100644
index 00000000000..f739db751a6
--- /dev/null
+++ b/app/assets/javascripts/how_to_merge.js
@@ -0,0 +1,12 @@
+document.addEventListener('DOMContentLoaded', () => {
+ const modal = $('#modal_merge_info').modal({
+ modal: true,
+ show: false,
+ });
+ $('.how_to_merge_link').bind('click', () => {
+ modal.show();
+ });
+ $('.modal-header .close').bind('click', () => {
+ modal.hide();
+ });
+});
diff --git a/app/assets/javascripts/lazy_loader.js b/app/assets/javascripts/lazy_loader.js
new file mode 100644
index 00000000000..3d64b121fa7
--- /dev/null
+++ b/app/assets/javascripts/lazy_loader.js
@@ -0,0 +1,76 @@
+/* eslint-disable one-export, one-var, one-var-declaration-per-line */
+
+import _ from 'underscore';
+
+export const placeholderImage = 'data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw==';
+const SCROLL_THRESHOLD = 300;
+
+export default class LazyLoader {
+ constructor(options = {}) {
+ this.lazyImages = [];
+ this.observerNode = options.observerNode || '#content-body';
+
+ const throttledScrollCheck = _.throttle(() => this.scrollCheck(), 300);
+ const debouncedElementsInView = _.debounce(() => this.checkElementsInView(), 300);
+
+ window.addEventListener('scroll', throttledScrollCheck);
+ window.addEventListener('resize', debouncedElementsInView);
+
+ const scrollContainer = options.scrollContainer || window;
+ scrollContainer.addEventListener('load', () => this.loadCheck());
+ }
+ searchLazyImages() {
+ this.lazyImages = [].slice.call(document.querySelectorAll('.lazy'));
+ this.checkElementsInView();
+ }
+ startContentObserver() {
+ const contentNode = document.querySelector(this.observerNode) || document.querySelector('body');
+
+ if (contentNode) {
+ const observer = new MutationObserver(() => this.searchLazyImages());
+
+ observer.observe(contentNode, {
+ childList: true,
+ subtree: true,
+ });
+ }
+ }
+ loadCheck() {
+ this.searchLazyImages();
+ this.startContentObserver();
+ }
+ scrollCheck() {
+ requestAnimationFrame(() => this.checkElementsInView());
+ }
+ checkElementsInView() {
+ const scrollTop = pageYOffset;
+ const visHeight = scrollTop + innerHeight + SCROLL_THRESHOLD;
+ let imgBoundRect, imgTop, imgBound;
+
+ // Loading Images which are in the current viewport or close to them
+ this.lazyImages = this.lazyImages.filter((selectedImage) => {
+ if (selectedImage.getAttribute('data-src')) {
+ imgBoundRect = selectedImage.getBoundingClientRect();
+
+ imgTop = scrollTop + imgBoundRect.top;
+ imgBound = imgTop + imgBoundRect.height;
+
+ if (scrollTop < imgBound && visHeight > imgTop) {
+ LazyLoader.loadImage(selectedImage);
+ return false;
+ }
+
+ return true;
+ }
+ return false;
+ });
+ }
+ static loadImage(img) {
+ if (img.getAttribute('data-src')) {
+ img.setAttribute('src', img.getAttribute('data-src'));
+ img.removeAttribute('data-src');
+ img.classList.remove('lazy');
+ img.classList.add('js-lazy-loaded');
+ }
+ }
+}
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index 26c67fb721c..e96d51de838 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -109,6 +109,7 @@ import './label_manager';
import './labels';
import './labels_select';
import './layout_nav';
+import LazyLoader from './lazy_loader';
import './line_highlighter';
import './logo';
import './member_expiration_date';
@@ -166,6 +167,11 @@ window.addEventListener('load', function onLoad() {
gl.utils.handleLocationHash();
}, false);
+gl.lazyLoader = new LazyLoader({
+ scrollContainer: window,
+ observerNode: '#content-body'
+});
+
$(function () {
var $body = $('body');
var $document = $(document);
@@ -284,13 +290,7 @@ $(function () {
return $container.remove();
// Commit show suppressed diff
});
- $('.navbar-toggle').on('click', function () {
- $('.header-content .title, .header-content .navbar-sub-nav').toggle();
- $('.header-content .header-logo').toggle();
- $('.header-content .navbar-collapse').toggle();
- $('.js-navbar-toggle-left, .js-navbar-toggle-right, .title-container').toggle();
- return $('.navbar-toggle').toggleClass('active');
- });
+ $('.navbar-toggle').on('click', () => $('.header-content').toggleClass('menu-expanded'));
// Show/hide comments on diff
$body.on('click', '.js-toggle-diff-comments', function (e) {
var $this = $(this);
diff --git a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
index c4e379a4a0b..8be7314ded8 100644
--- a/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
+++ b/app/assets/javascripts/merge_conflicts/merge_conflict_store.js
@@ -175,7 +175,7 @@ import Cookies from 'js-cookie';
getConflictsCountText() {
const count = this.getConflictsCount();
- const text = count ? 'conflicts' : 'conflict';
+ const text = count > 1 ? 'conflicts' : 'conflict';
return `${count} ${text}`;
},
diff --git a/app/assets/javascripts/star.js b/app/assets/javascripts/star.js
index 6d38124f1c1..3a06b477d7c 100644
--- a/app/assets/javascripts/star.js
+++ b/app/assets/javascripts/star.js
@@ -1,6 +1,8 @@
/* eslint-disable func-names, space-before-function-paren, wrap-iife, no-unused-vars, one-var, no-var, one-var-declaration-per-line, prefer-arrow-callback, no-new, max-len */
/* global Flash */
+import { __, s__ } from './locale';
+
export default class Star {
constructor() {
$('.project-home-panel .toggle-star').on('ajax:success', function(e, data, status, xhr) {
@@ -11,10 +13,10 @@ export default class Star {
toggleStar = function(isStarred) {
$this.parent().find('.star-count').text(data.star_count);
if (isStarred) {
- $starSpan.removeClass('starred').text('Star');
+ $starSpan.removeClass('starred').text(s__('StarProject|Star'));
$starIcon.removeClass('fa-star').addClass('fa-star-o');
} else {
- $starSpan.addClass('starred').text('Unstar');
+ $starSpan.addClass('starred').text(__('Unstar'));
$starIcon.removeClass('fa-star-o').addClass('fa-star');
}
};
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
index e8e22ad93a5..744a1cd24fa 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_deployment.js
@@ -108,7 +108,8 @@ export default {
</div>
<mr-widget-memory-usage
v-if="deployment.metrics_url"
- :metricsUrl="deployment.metrics_url"
+ :metrics-url="deployment.metrics_url"
+ :metrics-monitoring-url="deployment.metrics_monitoring_url"
/>
</div>
</div>
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
index 76cb71b6c12..534e2a88eff 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_memory_usage.js
@@ -7,7 +7,14 @@ import MRWidgetService from '../services/mr_widget_service';
export default {
name: 'MemoryUsage',
props: {
- metricsUrl: { type: String, required: true },
+ metricsUrl: {
+ type: String,
+ required: true,
+ },
+ metricsMonitoringUrl: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -124,7 +131,7 @@ export default {
<p
v-if="shouldShowMemoryGraph"
class="usage-info js-usage-info">
- Memory usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
+ <a :href="metricsMonitoringUrl">Memory</a> usage <b>{{memoryChangeType}}</b> from {{memoryFrom}}MB to {{memoryTo}}MB
</p>
<p
v-if="shouldShowLoadFailure"
diff --git a/app/assets/stylesheets/framework/avatar.scss b/app/assets/stylesheets/framework/avatar.scss
index 06f7af33f94..0dfa7a31d31 100644
--- a/app/assets/stylesheets/framework/avatar.scss
+++ b/app/assets/stylesheets/framework/avatar.scss
@@ -35,6 +35,8 @@
width: 40px;
height: 40px;
padding: 0;
+ background: $avatar-background;
+ overflow: hidden;
&.avatar-inline {
float: none;
diff --git a/app/assets/stylesheets/framework/calendar.scss b/app/assets/stylesheets/framework/calendar.scss
index 759401a7806..0ac095f7d8f 100644
--- a/app/assets/stylesheets/framework/calendar.scss
+++ b/app/assets/stylesheets/framework/calendar.scss
@@ -93,7 +93,7 @@
.is-selected .pika-day,
.pika-day:hover,
- .is-today .pika-day:hover {
+ .is-today .pika-day {
background: $gl-primary;
color: $white-light;
box-shadow: none;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index c7c2684d548..8ad082f7a65 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -163,8 +163,18 @@
td.blame-commit {
padding: 5px 10px;
min-width: 400px;
+ max-width: 400px;
background: $gray-light;
border-left: 3px solid;
+
+ .commit-row-title {
+ display: flex;
+ }
+
+ .item-title {
+ flex: 1;
+ margin-right: 0.5em;
+ }
}
@for $i from 0 through 5 {
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index 20fb10c28d4..605f4284bb5 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -132,6 +132,22 @@ header {
}
}
+ &.navbar-gitlab-new {
+ .fa-times {
+ display: none;
+ }
+
+ .menu-expanded {
+ .fa-ellipsis-v {
+ display: none;
+ }
+
+ .fa-times {
+ display: block;
+ }
+ }
+ }
+
.global-dropdown {
position: absolute;
left: -10px;
@@ -171,6 +187,19 @@ header {
min-height: $header-height;
padding-left: 30px;
+ &.menu-expanded {
+ @media (max-width: $screen-xs-max) {
+ .header-logo,
+ .title-container {
+ display: none;
+ }
+
+ .navbar-collapse {
+ display: block;
+ }
+ }
+ }
+
.dropdown-menu {
margin-top: -5px;
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index 99eea97377c..35b4d77a5ab 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -182,6 +182,12 @@
}
}
+ &.nav-controls-new-nav {
+ > .dropdown {
+ margin-right: 0;
+ }
+ }
+
> .btn-grouped {
float: none;
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 8a58c1ed567..befd8133be0 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -11,8 +11,17 @@
}
img {
- max-width: 100%;
+ /*max-width: 100%;*/
margin: 0 0 8px;
+ min-width: 200px;
+ min-height: 100px;
+ background-color: $gray-lightest;
+ }
+
+ img.js-lazy-loaded {
+ min-width: none;
+ min-height: none;
+ background-color: none;
}
p a:not(.no-attachment-icon) img {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 7016208f624..cf0a1ad57d0 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -379,7 +379,9 @@ $issue-boards-card-shadow: rgba(186, 186, 186, 0.5);
* Avatar
*/
$avatar_radius: 50%;
-$avatar-border: $border-color;
+$avatar-border: $gray-normal;
+$avatar-border-hover: $gray-darker;
+$avatar-background: $gray-lightest;
$gl-avatar-size: 40px;
/*
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 9f3e278ebfc..360ffda8d71 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -21,6 +21,11 @@ header.navbar-gitlab-new {
padding-right: 0;
color: currentColor;
+ img {
+ height: 28px;
+ margin-right: 10px;
+ }
+
> a {
display: flex;
align-items: center;
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index df858cffe09..6039cda96d8 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -431,7 +431,10 @@
margin: 5px;
}
-.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar {
+.page-with-layout-nav.page-with-sub-nav .issue-boards-sidebar,
+.page-with-new-sidebar.page-with-sidebar .issue-boards-sidebar {
+ position: absolute;
+
&.right-sidebar {
top: 0;
bottom: 0;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index 9637d26e56d..d3862df20d3 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -597,7 +597,7 @@
}
// Dropdown button in mini pipeline graph
-.mini-pipeline-graph-dropdown-toggle {
+button.mini-pipeline-graph-dropdown-toggle {
border-radius: 100px;
background-color: $white-light;
border-width: 1px;
@@ -608,6 +608,7 @@
padding: 0;
transition: all 0.2s linear;
position: relative;
+ vertical-align: middle;
> .fa.fa-caret-down {
position: absolute;
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index c1bc4c0d675..4c0f7556894 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -76,11 +76,11 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
params.delete(:domain_blacklist_raw) if params[:domain_blacklist_file]
params.require(:application_setting).permit(
- application_setting_params_ce
+ application_setting_params_attributes
)
end
- def application_setting_params_ce
+ def application_setting_params_attributes
[
:admin_notification_email,
:after_sign_out_path,
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 8360ce08bdc..05e749c00c0 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,6 +1,6 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.with_route.limit(10)
+ @projects = Project.without_deleted.with_route.limit(10)
@users = User.limit(10)
@groups = Group.with_route.limit(10)
end
diff --git a/app/controllers/admin/projects_controller.rb b/app/controllers/admin/projects_controller.rb
index 984d5398708..0b6cd71e651 100644
--- a/app/controllers/admin/projects_controller.rb
+++ b/app/controllers/admin/projects_controller.rb
@@ -3,18 +3,9 @@ class Admin::ProjectsController < Admin::ApplicationController
before_action :group, only: [:show, :transfer]
def index
- params[:sort] ||= 'latest_activity_desc'
- @projects = Project.with_statistics
- @projects = @projects.in_namespace(params[:namespace_id]) if params[:namespace_id].present?
- @projects = @projects.where(visibility_level: params[:visibility_level]) if params[:visibility_level].present?
- @projects = @projects.with_push if params[:with_push].present?
- @projects = @projects.abandoned if params[:abandoned].present?
- @projects = @projects.where(last_repository_check_failed: true) if params[:last_repository_check_failed].present?
- @projects = @projects.non_archived unless params[:archived].present?
- @projects = @projects.personal(current_user) if params[:personal].present?
- @projects = @projects.search(params[:name]) if params[:name].present?
- @projects = @projects.sort(@sort = params[:sort])
- @projects = @projects.includes(:namespace).order("namespaces.path, projects.name ASC").page(params[:page])
+ finder = Admin::ProjectsFinder.new(params: params, current_user: current_user)
+ @projects = finder.execute
+ @sort = finder.sort
respond_to do |format|
format.html
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 95de3a44641..221e01b415a 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -22,6 +22,7 @@ class Projects::ApplicationController < ApplicationController
def project
return @project if @project
+ return nil unless params[:project_id] || params[:id]
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index 0ac9da2ff0f..e2ccabb22db 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -8,7 +8,6 @@ class Projects::IssuesController < Projects::ApplicationController
prepend_before_action :authenticate_user!, only: [:new]
- before_action :redirect_to_external_issue_tracker, only: [:index, :new]
before_action :check_issues_available!
before_action :issue, except: [:index, :new, :create, :bulk_update]
@@ -243,19 +242,19 @@ class Projects::IssuesController < Projects::ApplicationController
end
def authorize_update_issue!
- return render_404 unless can?(current_user, :update_issue, @issue)
+ render_404 unless can?(current_user, :update_issue, @issue)
end
def authorize_admin_issues!
- return render_404 unless can?(current_user, :admin_issue, @project)
+ render_404 unless can?(current_user, :admin_issue, @project)
end
def authorize_create_merge_request!
- return render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
+ render_404 unless can?(current_user, :push_code, @project) && @issue.can_be_worked_on?(current_user)
end
def check_issues_available!
- return render_404 unless @project.feature_available?(:issues, current_user) && @project.default_issues_tracker?
+ return render_404 unless @project.feature_available?(:issues, current_user)
end
def redirect_to_external_issue_tracker
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index 70c41da4de5..d361e661d0e 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -223,12 +223,18 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
if can?(current_user, :read_environment, environment) && environment.has_metrics?
metrics_project_environment_deployment_path(environment.project, environment, deployment)
end
+
+ metrics_monitoring_url =
+ if can?(current_user, :read_environment, environment)
+ environment_metrics_path(environment)
+ end
{
id: environment.id,
name: environment.name,
url: project_environment_path(project, environment),
metrics_url: metrics_url,
+ metrics_monitoring_url: metrics_monitoring_url,
stop_url: stop_url,
external_url: environment.external_url,
external_url_formatted: environment.formatted_external_url,
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index c769693255c..2d7cbd4614e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -296,10 +296,10 @@ class ProjectsController < Projects::ApplicationController
def project_params
params.require(:project)
- .permit(project_params_ce)
+ .permit(project_params_attributes)
end
- def project_params_ce
+ def project_params_attributes
[
:avatar,
:build_allow_git_fetch,
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 0e8a57f8e03..69513f4dadc 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -5,6 +5,14 @@ class SessionsController < Devise::SessionsController
skip_before_action :check_two_factor_requirement, only: [:destroy]
+ # Explicitly call protect from forgery before anything else. Otherwise the
+ # CSFR-token might be cleared before authentication is done. This was the case
+ # when LDAP was enabled and the `OmniauthCallbacksController` is loaded
+ #
+ # *Note:* `prepend: true` is the default for rails4, but this will be changed
+ # to `prepend: false` in rails5.
+ protect_from_forgery prepend: true, with: :exception
+
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
if: :two_factor_enabled?, only: [:create]
diff --git a/app/finders/admin/projects_finder.rb b/app/finders/admin/projects_finder.rb
new file mode 100644
index 00000000000..a5ba791a513
--- /dev/null
+++ b/app/finders/admin/projects_finder.rb
@@ -0,0 +1,33 @@
+class Admin::ProjectsFinder
+ attr_reader :sort, :namespace_id, :visibility_level, :with_push,
+ :abandoned, :last_repository_check_failed, :archived,
+ :personal, :name, :page, :current_user
+
+ def initialize(params:, current_user:)
+ @current_user = current_user
+ @sort = params.fetch(:sort) { 'latest_activity_desc' }
+ @namespace_id = params[:namespace_id]
+ @visibility_level = params[:visibility_level]
+ @with_push = params[:with_push]
+ @abandoned = params[:abandoned]
+ @last_repository_check_failed = params[:last_repository_check_failed]
+ @archived = params[:archived]
+ @personal = params[:personal]
+ @name = params[:name]
+ @page = params[:page]
+ end
+
+ def execute
+ items = Project.with_statistics
+ items = items.in_namespace(namespace_id) if namespace_id.present?
+ items = items.where(visibility_level: visibility_level) if visibility_level.present?
+ items = items.with_push if with_push.present?
+ items = items.abandoned if abandoned.present?
+ items = items.where(last_repository_check_failed: true) if last_repository_check_failed.present?
+ items = items.non_archived unless archived.present?
+ items = items.personal(current_user) if personal.present?
+ items = items.search(name) if name.present?
+ items = items.sort(sort)
+ items.includes(:namespace).order("namespaces.path, projects.name ASC").page(page)
+ end
+end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index bbe7f3c8fb4..0e068d4b51c 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -11,17 +11,12 @@ module AvatarsHelper
def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
- css_class = options[:css_class] || ''
avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
data_attributes = { container: 'body' }
- if options[:lazy]
- data_attributes[:src] = avatar_url
- end
-
image_tag(
- options[:lazy] ? '' : avatar_url,
- class: "avatar has-tooltip s#{avatar_size} #{css_class}",
+ avatar_url,
+ class: %W[avatar has-tooltip s#{avatar_size}].push(*options[:css_class]),
alt: "#{user_name}'s avatar",
title: user_name,
data: data_attributes
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index fdbca789d21..5f11fe62030 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -61,8 +61,8 @@ module EmailsHelper
else
image_tag(
image_url('mailers/gitlab_header_logo.gif'),
- size: "55x50",
- alt: "GitLab"
+ size: '55x50',
+ alt: 'GitLab'
)
end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 42b6cfdf02f..7e1ccb23e9e 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -17,10 +17,10 @@ module IssuesHelper
return '' if project.nil?
url =
- if options[:only_path]
- project.issues_tracker.issue_path(issue_iid)
+ if options[:internal]
+ url_for_internal_issue(issue_iid, project, options)
else
- project.issues_tracker.issue_url(issue_iid)
+ url_for_tracker_issue(issue_iid, project, options)
end
# Ensure we return a valid URL to prevent possible XSS.
@@ -29,6 +29,24 @@ module IssuesHelper
''
end
+ def url_for_tracker_issue(issue_iid, project, options)
+ if options[:only_path]
+ project.issues_tracker.issue_path(issue_iid)
+ else
+ project.issues_tracker.issue_url(issue_iid)
+ end
+ end
+
+ def url_for_internal_issue(issue_iid, project = @project, options = {})
+ helpers = Gitlab::Routing.url_helpers
+
+ if options[:only_path]
+ helpers.namespace_project_issue_path(namespace_id: project.namespace, project_id: project, id: issue_iid)
+ else
+ helpers.namespace_project_issue_url(namespace_id: project.namespace, project_id: project, id: issue_iid)
+ end
+ end
+
def bulk_update_milestone_options
milestones = @project.milestones.active.reorder(due_date: :asc, title: :asc).to_a
milestones.unshift(Milestone::None)
@@ -158,4 +176,6 @@ module IssuesHelper
# Required for Banzai::Filter::IssueReferenceFilter
module_function :url_for_issue
+ module_function :url_for_internal_issue
+ module_function :url_for_tracker_issue
end
diff --git a/app/helpers/lazy_image_tag_helper.rb b/app/helpers/lazy_image_tag_helper.rb
new file mode 100644
index 00000000000..2c5619ac41b
--- /dev/null
+++ b/app/helpers/lazy_image_tag_helper.rb
@@ -0,0 +1,24 @@
+module LazyImageTagHelper
+ def placeholder_image
+ "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+ end
+
+ # Override the default ActionView `image_tag` helper to support lazy-loading
+ def image_tag(source, options = {})
+ options = options.symbolize_keys
+
+ unless options.delete(:lazy) == false
+ options[:data] ||= {}
+ options[:data][:src] = path_to_image(source)
+ options[:class] ||= ""
+ options[:class] << " lazy"
+
+ source = placeholder_image
+ end
+
+ super(source, options)
+ end
+
+ # Required for Banzai::Filter::ImageLazyLoadFilter
+ module_function :placeholder_image
+end
diff --git a/app/helpers/system_note_helper.rb b/app/helpers/system_note_helper.rb
index 209bd56b78a..08fd97cd048 100644
--- a/app/helpers/system_note_helper.rb
+++ b/app/helpers/system_note_helper.rb
@@ -18,7 +18,8 @@ module SystemNoteHelper
'milestone' => 'icon_clock_o',
'discussion' => 'icon_comment_o',
'moved' => 'icon_arrow_circle_o_right',
- 'outdated' => 'icon_edit'
+ 'outdated' => 'icon_edit',
+ 'duplicate' => 'icon_clone'
}.freeze
def icon_for_system_note(note)
diff --git a/app/helpers/version_check_helper.rb b/app/helpers/version_check_helper.rb
index 456598b4c28..3b175251446 100644
--- a/app/helpers/version_check_helper.rb
+++ b/app/helpers/version_check_helper.rb
@@ -2,7 +2,7 @@ module VersionCheckHelper
def version_status_badge
if Rails.env.production? && current_application_settings.version_check_enabled
image_url = VersionCheck.new.url
- image_tag image_url, class: 'js-version-status-badge'
+ image_tag image_url, class: 'js-version-status-badge', lazy: false
end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index b646b32fc64..e5b615a7cc0 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -21,7 +21,7 @@ module Ci
has_many :merge_requests, foreign_key: "head_pipeline_id"
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build'
- has_many :retryable_builds, -> { latest.failed_or_canceled }, foreign_key: :commit_id, class_name: 'Ci::Build'
+ has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :cancelable_statuses, -> { cancelable }, foreign_key: :commit_id, class_name: 'CommitStatus'
has_many :manual_actions, -> { latest.manual_actions.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
has_many :artifacts, -> { latest.with_artifacts_not_expired.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build'
diff --git a/app/models/ci/pipeline_schedule.rb b/app/models/ci/pipeline_schedule.rb
index e4ae1b35f66..085eeeae157 100644
--- a/app/models/ci/pipeline_schedule.rb
+++ b/app/models/ci/pipeline_schedule.rb
@@ -40,10 +40,6 @@ module Ci
update_attribute(:active, false)
end
- def runnable_by_owner?
- Ability.allowed?(owner, :create_pipeline, project)
- end
-
def set_next_run_at
self.next_run_at = Gitlab::Ci::CronParser.new(cron, cron_timezone).next_time_from(Time.now)
end
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index 95152dcd68c..48547a938fc 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -11,7 +11,7 @@ module CacheMarkdownField
extend ActiveSupport::Concern
# Increment this number every time the renderer changes its output
- CACHE_VERSION = 1
+ CACHE_VERSION = 2
# changes to these attributes cause the cache to be invalidates
INVALIDATED_BY = %w[author project].freeze
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb
index fc6b840f7a8..ef95d6b0f98 100644
--- a/app/models/concerns/protected_ref.rb
+++ b/app/models/concerns/protected_ref.rb
@@ -17,7 +17,13 @@ module ProtectedRef
class_methods do
def protected_ref_access_levels(*types)
types.each do |type|
- has_many :"#{type}_access_levels", dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
+ # We need to set `inverse_of` to make sure the `belongs_to`-object is set
+ # when creating children using `accepts_nested_attributes_for`.
+ #
+ # If we don't `protected_branch` or `protected_tag` would be empty and
+ # `project` cannot be delegated to it, which in turn would cause validations
+ # to fail.
+ has_many :"#{type}_access_levels", dependent: :destroy, inverse_of: self.model_name.singular # rubocop:disable Cop/ActiveRecordDependent
validates :"#{type}_access_levels", length: { is: 1, message: "are restricted to a single instance per #{self.model_name.human}." }
@@ -25,8 +31,8 @@ module ProtectedRef
end
end
- def protected_ref_accessible_to?(ref, user, action:)
- access_levels_for_ref(ref, action: action).any? do |access_level|
+ def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil)
+ access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level|
access_level.check_access(user)
end
end
@@ -37,8 +43,9 @@ module ProtectedRef
end
end
- def access_levels_for_ref(ref, action:)
- self.matching(ref).map(&:"#{action}_access_levels").flatten
+ def access_levels_for_ref(ref, action:, protected_refs: nil)
+ self.matching(ref, protected_refs: protected_refs)
+ .map(&:"#{action}_access_levels").flatten
end
def matching(ref_name, protected_refs: nil)
diff --git a/app/models/group.rb b/app/models/group.rb
index dfa4e8adedd..bd5735ed82e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -167,10 +167,14 @@ class Group < Namespace
end
def has_owner?(user)
+ return false unless user
+
members_with_parents.owners.where(user_id: user).any?
end
def has_master?(user)
+ return false unless user
+
members_with_parents.masters.where(user_id: user).any?
end
@@ -212,7 +216,7 @@ class Group < Namespace
end
def members_with_parents
- GroupMember.non_request.where(source_id: ancestors.pluck(:id).push(id))
+ GroupMember.active.where(source_id: ancestors.pluck(:id).push(id)).where.not(user_id: nil)
end
def users_with_parents
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index e4e7999d0f2..a910099b4c1 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -596,7 +596,7 @@ class MergeRequest < ActiveRecord::Base
# running `ReferenceExtractor` on each of them separately.
# This optimization does not apply to issues from external sources.
def cache_merge_request_closes_issues!(current_user)
- return if project.has_external_issue_tracker?
+ return unless project.issues_enabled?
transaction do
self.merge_requests_closing_issues.delete_all
diff --git a/app/models/project.rb b/app/models/project.rb
index 0b357d5d003..d827bfaa806 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -734,9 +734,11 @@ class Project < ActiveRecord::Base
end
def get_issue(issue_id, current_user)
- if default_issues_tracker?
- IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id)
- else
+ issue = IssuesFinder.new(current_user, project_id: id).find_by(iid: issue_id) if issues_enabled?
+
+ if issue
+ issue
+ elsif external_issue_tracker
ExternalIssue.new(issue_id, self)
end
end
@@ -758,7 +760,7 @@ class Project < ActiveRecord::Base
end
def external_issue_reference_pattern
- external_issue_tracker.class.reference_pattern
+ external_issue_tracker.class.reference_pattern(only_long: issues_enabled?)
end
def default_issues_tracker?
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index 6d6a3ae3647..31984c5d7ed 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -8,8 +8,12 @@ class IssueTrackerService < Service
# This pattern does not support cross-project references
# The other code assumes that this pattern is a superset of all
# overriden patterns. See ReferenceRegexes::EXTERNAL_PATTERN
- def self.reference_pattern
- @reference_pattern ||= %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+ def self.reference_pattern(only_long: false)
+ if only_long
+ %r{(\b[A-Z][A-Z0-9_]+-)(?<issue>\d+)}
+ else
+ %r{(\b[A-Z][A-Z0-9_]+-|#{Issue.reference_prefix})(?<issue>\d+)}
+ end
end
def default?
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 5498a2e17b2..37f2c96a22f 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -3,10 +3,8 @@ class JiraService < IssueTrackerService
validates :url, url: true, presence: true, if: :activated?
validates :api_url, url: true, allow_blank: true
- validates :project_key, presence: true, if: :activated?
- prop_accessor :username, :password, :url, :api_url, :project_key,
- :jira_issue_transition_id, :title, :description
+ prop_accessor :username, :password, :url, :api_url, :jira_issue_transition_id, :title, :description
before_update :reset_password
@@ -18,7 +16,7 @@ class JiraService < IssueTrackerService
end
# {PROJECT-KEY}-{NUMBER} Examples: JIRA-1, PROJECT-1
- def self.reference_pattern
+ def self.reference_pattern(only_long: true)
@reference_pattern ||= %r{(?<issue>\b([A-Z][A-Z0-9_]+-)\d+)}
end
@@ -54,10 +52,6 @@ class JiraService < IssueTrackerService
@client ||= JIRA::Client.new(options)
end
- def jira_project
- @jira_project ||= jira_request { client.Project.find(project_key) }
- end
-
def help
"You need to configure JIRA before enabling this service. For more details
read the
@@ -88,18 +82,12 @@ class JiraService < IssueTrackerService
[
{ type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
- { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true },
{ type: 'text', name: 'username', placeholder: '', required: true },
{ type: 'password', name: 'password', placeholder: '', required: true },
- { type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
+ { type: 'text', name: 'jira_issue_transition_id', title: 'Transition ID', placeholder: '' }
]
end
- # URLs to redirect from Gitlab issues pages to jira issue tracker
- def project_url
- "#{url}/issues/?jql=project=#{project_key}"
- end
-
def issues_url
"#{url}/browse/:id"
end
@@ -184,7 +172,7 @@ class JiraService < IssueTrackerService
def test_settings
return unless client_url.present?
# Test settings by getting the project
- jira_request { jira_project.present? }
+ jira_request { client.ServerInfo.all.attrs }
end
private
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 8663cf5e602..d27eeff9fb4 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -471,8 +471,17 @@ class Repository
end
cache_method :root_ref
+ # Gitaly migration: https://gitlab.com/gitlab-org/gitaly/issues/314
def exists?
- refs_directory_exists?
+ return false unless path_with_namespace
+
+ Gitlab::GitalyClient.migrate(:repository_exists) do |enabled|
+ if enabled
+ raw_repository.exists?
+ else
+ refs_directory_exists?
+ end
+ end
end
cache_method :exists?
@@ -1095,8 +1104,6 @@ class Repository
end
def refs_directory_exists?
- return false unless path_with_namespace
-
File.exist?(File.join(path_to_repo, 'refs'))
end
diff --git a/app/models/system_note_metadata.rb b/app/models/system_note_metadata.rb
index 414c95f7705..0b33e45473b 100644
--- a/app/models/system_note_metadata.rb
+++ b/app/models/system_note_metadata.rb
@@ -1,7 +1,8 @@
class SystemNoteMetadata < ActiveRecord::Base
ICON_TYPES = %w[
commit description merge confidential visible label assignee cross_reference
- title time_tracking branch milestone discussion task moved opened closed merged
+ title time_tracking branch milestone discussion task moved
+ opened closed merged duplicate
outdated
].freeze
diff --git a/app/policies/ci/build_policy.rb b/app/policies/ci/build_policy.rb
index 386822d3ff6..984e5482288 100644
--- a/app/policies/ci/build_policy.rb
+++ b/app/policies/ci/build_policy.rb
@@ -1,17 +1,15 @@
module Ci
class BuildPolicy < CommitStatusPolicy
- condition(:protected_action) do
- next false unless @subject.action?
-
+ condition(:protected_ref) do
access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
if @subject.tag?
!access.can_create_tag?(@subject.ref)
else
- !access.can_merge_to_branch?(@subject.ref)
+ !access.can_update_branch?(@subject.ref)
end
end
- rule { protected_action }.prevent :update_build
+ rule { protected_ref }.prevent :update_build
end
end
diff --git a/app/policies/ci/pipeline_policy.rb b/app/policies/ci/pipeline_policy.rb
index a2dde95dbc8..4e689a9efd5 100644
--- a/app/policies/ci/pipeline_policy.rb
+++ b/app/policies/ci/pipeline_policy.rb
@@ -1,5 +1,17 @@
module Ci
class PipelinePolicy < BasePolicy
delegate { @subject.project }
+
+ condition(:protected_ref) do
+ access = ::Gitlab::UserAccess.new(@user, project: @subject.project)
+
+ if @subject.tag?
+ !access.can_create_tag?(@subject.ref)
+ else
+ !access.can_update_branch?(@subject.ref)
+ end
+ end
+
+ rule { protected_ref }.prevent :update_pipeline
end
end
diff --git a/app/policies/global_policy.rb b/app/policies/global_policy.rb
index 55eefa76d3f..1c91425f589 100644
--- a/app/policies/global_policy.rb
+++ b/app/policies/global_policy.rb
@@ -44,7 +44,7 @@ class GlobalPolicy < BasePolicy
prevent :log_in
end
- rule { ~restricted_public_level }.policy do
+ rule { admin | ~restricted_public_level }.policy do
enable :read_users_list
end
end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 323131c0f7e..0133091db57 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -10,7 +10,8 @@ class ProjectPolicy < BasePolicy
desc "User is a project owner"
condition :owner do
- @user && project.owner == @user || (project.group && project.group.has_owner?(@user))
+ (project.owner.present? && project.owner == @user) ||
+ project.group&.has_owner?(@user)
end
desc "Project has public builds enabled"
@@ -287,9 +288,6 @@ class ProjectPolicy < BasePolicy
prevent :create_issue
prevent :update_issue
prevent :admin_issue
- end
-
- rule { issues_disabled & default_issues_tracker }.policy do
prevent :read_issue
end
diff --git a/app/serializers/build_details_entity.rb b/app/serializers/build_details_entity.rb
index 20f9938f038..743a08acefe 100644
--- a/app/serializers/build_details_entity.rb
+++ b/app/serializers/build_details_entity.rb
@@ -16,7 +16,8 @@ class BuildDetailsEntity < JobEntity
end
expose :path do |build|
- project_merge_request_path(project, build.merge_request)
+ project_merge_request_path(build.merge_request.project,
+ build.merge_request)
end
end
diff --git a/app/serializers/deploy_key_entity.rb b/app/serializers/deploy_key_entity.rb
index 068013c8829..c75431a79ae 100644
--- a/app/serializers/deploy_key_entity.rb
+++ b/app/serializers/deploy_key_entity.rb
@@ -9,7 +9,7 @@ class DeployKeyEntity < Grape::Entity
expose :created_at
expose :updated_at
expose :projects, using: ProjectEntity do |deploy_key|
- deploy_key.projects.select { |project| options[:user].can?(:read_project, project) }
+ deploy_key.projects.without_deleted.select { |project| options[:user].can?(:read_project, project) }
end
expose :can_edit
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 273386776fa..21e2ef153de 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -15,12 +15,40 @@ module Ci
pipeline_schedule: schedule
)
+ result = validate(current_user || trigger_request.trigger.owner,
+ ignore_skip_ci: ignore_skip_ci,
+ save_on_errors: save_on_errors)
+
+ return result if result
+
+ Ci::Pipeline.transaction do
+ update_merge_requests_head_pipeline if pipeline.save
+
+ Ci::CreatePipelineStagesService
+ .new(project, current_user)
+ .execute(pipeline)
+ end
+
+ cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
+
+ pipeline_created_counter.increment(source: source)
+
+ pipeline.tap(&:process!)
+ end
+
+ private
+
+ def validate(triggering_user, ignore_skip_ci:, save_on_errors:)
unless project.builds_enabled?
return error('Pipeline is disabled')
end
- unless trigger_request || can?(current_user, :create_pipeline, project)
- return error('Insufficient permissions to create a new pipeline')
+ unless allowed_to_trigger_pipeline?(triggering_user)
+ if can?(triggering_user, :create_pipeline, project)
+ return error("Insufficient permissions for protected ref '#{ref}'")
+ else
+ return error('Insufficient permissions to create a new pipeline')
+ end
end
unless branch? || tag?
@@ -46,24 +74,29 @@ module Ci
unless pipeline.has_stage_seeds?
return error('No stages / jobs for this pipeline.')
end
+ end
- Ci::Pipeline.transaction do
- update_merge_requests_head_pipeline if pipeline.save
-
- Ci::CreatePipelineStagesService
- .new(project, current_user)
- .execute(pipeline)
+ def allowed_to_trigger_pipeline?(triggering_user)
+ if triggering_user
+ allowed_to_create?(triggering_user)
+ else # legacy triggers don't have a corresponding user
+ !project.protected_for?(ref)
end
+ end
- cancel_pending_pipelines if project.auto_cancel_pending_pipelines?
-
- pipeline_created_counter.increment(source: source)
+ def allowed_to_create?(triggering_user)
+ access = Gitlab::UserAccess.new(triggering_user, project: project)
- pipeline.tap(&:process!)
+ can?(triggering_user, :create_pipeline, project) &&
+ if branch?
+ access.can_update_branch?(ref)
+ elsif tag?
+ access.can_create_tag?(ref)
+ else
+ true # Allow it for now and we'll reject when we check ref existence
+ end
end
- private
-
def update_merge_requests_head_pipeline
return unless pipeline.latest?
@@ -113,15 +146,21 @@ module Ci
end
def branch?
- project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
+ return @is_branch if defined?(@is_branch)
+
+ @is_branch =
+ project.repository.ref_exists?(Gitlab::Git::BRANCH_REF_PREFIX + ref)
end
def tag?
- project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
+ return @is_tag if defined?(@is_tag)
+
+ @is_tag =
+ project.repository.ref_exists?(Gitlab::Git::TAG_REF_PREFIX + ref)
end
def ref
- Gitlab::Git.ref_name(origin_ref)
+ @ref ||= Gitlab::Git.ref_name(origin_ref)
end
def valid_sha?
diff --git a/app/services/ci/create_trigger_request_service.rb b/app/services/ci/create_trigger_request_service.rb
index cf3d4aee2bc..a43d0e4593c 100644
--- a/app/services/ci/create_trigger_request_service.rb
+++ b/app/services/ci/create_trigger_request_service.rb
@@ -1,12 +1,14 @@
module Ci
- class CreateTriggerRequestService
- def execute(project, trigger, ref, variables = nil)
+ module CreateTriggerRequestService
+ Result = Struct.new(:trigger_request, :pipeline)
+
+ def self.execute(project, trigger, ref, variables = nil)
trigger_request = trigger.trigger_requests.create(variables: variables)
pipeline = Ci::CreatePipelineService.new(project, trigger.owner, ref: ref)
.execute(:trigger, ignore_skip_ci: true, trigger_request: trigger_request)
- trigger_request if pipeline.persisted?
+ Result.new(trigger_request, pipeline)
end
end
end
diff --git a/app/services/issuable_base_service.rb b/app/services/issuable_base_service.rb
index 9078b1f0983..ea497729115 100644
--- a/app/services/issuable_base_service.rb
+++ b/app/services/issuable_base_service.rb
@@ -58,6 +58,7 @@ class IssuableBaseService < BaseService
params.delete(:assignee_ids)
params.delete(:assignee_id)
params.delete(:due_date)
+ params.delete(:canonical_issue_id)
end
filter_assignee(issuable)
diff --git a/app/services/issues/base_service.rb b/app/services/issues/base_service.rb
index 34199eb5d13..4c198fc96ea 100644
--- a/app/services/issues/base_service.rb
+++ b/app/services/issues/base_service.rb
@@ -7,6 +7,14 @@ module Issues
issue_data
end
+ def reopen_service
+ Issues::ReopenService
+ end
+
+ def close_service
+ Issues::CloseService
+ end
+
private
def create_assignee_note(issue, old_assignees)
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ddef5281498..74459c3342c 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -16,13 +16,13 @@ module Issues
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
def close_issue(issue, commit: nil, notifications: true, system_note: true)
- if project.jira_tracker? && project.jira_service.active
+ if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue)
project.jira_service.close_issue(commit, issue)
todo_service.close_issue(issue, current_user)
return issue
end
- if project.default_issues_tracker? && issue.close
+ if project.issues_enabled? && issue.close
event_service.close_issue(issue, current_user)
create_note(issue, commit) if system_note
notification_service.close_issue(issue, current_user) if notifications
diff --git a/app/services/issues/duplicate_service.rb b/app/services/issues/duplicate_service.rb
new file mode 100644
index 00000000000..5c0854e664d
--- /dev/null
+++ b/app/services/issues/duplicate_service.rb
@@ -0,0 +1,24 @@
+module Issues
+ class DuplicateService < Issues::BaseService
+ def execute(duplicate_issue, canonical_issue)
+ return if canonical_issue == duplicate_issue
+ return unless can?(current_user, :update_issue, duplicate_issue)
+ return unless can?(current_user, :create_note, canonical_issue)
+
+ create_issue_duplicate_note(duplicate_issue, canonical_issue)
+ create_issue_canonical_note(canonical_issue, duplicate_issue)
+
+ close_service.new(project, current_user, {}).execute(duplicate_issue)
+ end
+
+ private
+
+ def create_issue_duplicate_note(duplicate_issue, canonical_issue)
+ SystemNoteService.mark_duplicate_issue(duplicate_issue, duplicate_issue.project, current_user, canonical_issue)
+ end
+
+ def create_issue_canonical_note(canonical_issue, duplicate_issue)
+ SystemNoteService.mark_canonical_issue_of_duplicate(canonical_issue, canonical_issue.project, current_user, duplicate_issue)
+ end
+ end
+end
diff --git a/app/services/issues/update_service.rb b/app/services/issues/update_service.rb
index cd9f9a4a16e..8d918ccc635 100644
--- a/app/services/issues/update_service.rb
+++ b/app/services/issues/update_service.rb
@@ -5,6 +5,7 @@ module Issues
def execute(issue)
handle_move_between_iids(issue)
filter_spam_check_params
+ change_issue_duplicate(issue)
update(issue)
end
@@ -53,14 +54,6 @@ module Issues
end
end
- def reopen_service
- Issues::ReopenService
- end
-
- def close_service
- Issues::CloseService
- end
-
def handle_move_between_iids(issue)
return unless params[:move_between_iids]
@@ -72,6 +65,15 @@ module Issues
issue.move_between(issue_before, issue_after)
end
+ def change_issue_duplicate(issue)
+ canonical_issue_id = params.delete(:canonical_issue_id)
+ canonical_issue = IssuesFinder.new(current_user).find_by(id: canonical_issue_id)
+
+ if canonical_issue
+ Issues::DuplicateService.new(project, current_user).execute(issue, canonical_issue)
+ end
+ end
+
private
def get_issue_if_allowed(project, iid)
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 30ca95eef7a..d81035e4eba 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -5,7 +5,7 @@ module Projects
return error('New visibility level not allowed!')
end
- if project.has_container_registry_tags?
+ if renaming_project_with_container_registry_tags?
return error('Cannot rename project because it contains container registry tags!')
end
@@ -44,6 +44,13 @@ module Projects
true
end
+ def renaming_project_with_container_registry_tags?
+ new_path = params[:path]
+
+ new_path && new_path != project.path &&
+ project.has_container_registry_tags?
+ end
+
def changing_default_branch?
new_branch = params[:default_branch]
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 6f82159e6c7..5dc1b91d2c0 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -471,6 +471,24 @@ module QuickActions
end
end
+ desc 'Mark this issue as a duplicate of another issue'
+ explanation do |duplicate_reference|
+ "Marks this issue as a duplicate of #{duplicate_reference}."
+ end
+ params '#issue'
+ condition do
+ issuable.is_a?(Issue) &&
+ issuable.persisted? &&
+ current_user.can?(:"update_#{issuable.to_ability_name}", issuable)
+ end
+ command :duplicate do |duplicate_param|
+ canonical_issue = extract_references(duplicate_param, :issue).first
+
+ if canonical_issue.present?
+ @updates[:canonical_issue_id] = canonical_issue.id
+ end
+ end
+
def extract_users(params)
return [] if params.nil?
diff --git a/app/services/system_note_service.rb b/app/services/system_note_service.rb
index da0f21d449a..2dbee9c246e 100644
--- a/app/services/system_note_service.rb
+++ b/app/services/system_note_service.rb
@@ -552,6 +552,44 @@ module SystemNoteService
create_note(NoteSummary.new(noteable, project, author, body, action: 'moved'))
end
+ # Called when a Noteable has been marked as a duplicate of another Issue
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # canonical_issue - Issue that this is a duplicate of
+ #
+ # Example Note text:
+ #
+ # "marked this issue as a duplicate of #1234"
+ #
+ # "marked this issue as a duplicate of other_project#5678"
+ #
+ # Returns the created Note object
+ def mark_duplicate_issue(noteable, project, author, canonical_issue)
+ body = "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
+ # Called when a Noteable has been marked as the canonical Issue of a duplicate
+ #
+ # noteable - Noteable object
+ # project - Project owning noteable
+ # author - User performing the change
+ # duplicate_issue - Issue that was a duplicate of this
+ #
+ # Example Note text:
+ #
+ # "marked #1234 as a duplicate of this issue"
+ #
+ # "marked other_project#5678 as a duplicate of this issue"
+ #
+ # Returns the created Note object
+ def mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue)
+ body = "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue"
+ create_note(NoteSummary.new(noteable, project, author, body, action: 'duplicate'))
+ end
+
private
def notes_for_mentioner(mentioner, noteable, notes)
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 128b5dc01ab..8e94e68bc11 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -150,7 +150,7 @@
.well-segment.well-centered
= link_to admin_groups_path do
%h3.text-center
- Groups
+ Groups:
= number_with_delimiter(Group.count)
%hr
= link_to 'New group', new_admin_group_path, class: "btn btn-new"
diff --git a/app/views/layouts/header/_new.html.haml b/app/views/layouts/header/_new.html.haml
index 76367bd9734..60940dba475 100644
--- a/app/views/layouts/header/_new.html.haml
+++ b/app/views/layouts/header/_new.html.haml
@@ -81,6 +81,6 @@
%button.navbar-toggle.hidden-sm.hidden-md.hidden-lg{ type: 'button' }
%span.sr-only Toggle navigation
= icon('ellipsis-v', class: 'js-navbar-toggle-right')
- = icon('times', class: 'js-navbar-toggle-left', style: 'display: none;')
+ = icon('times', class: 'js-navbar-toggle-left')
= render 'shared/outdated_browser'
diff --git a/app/views/layouts/nav/_new_project_sidebar.html.haml b/app/views/layouts/nav/_new_project_sidebar.html.haml
index 21f175291fa..00395b222e4 100644
--- a/app/views/layouts/nav/_new_project_sidebar.html.haml
+++ b/app/views/layouts/nav/_new_project_sidebar.html.haml
@@ -75,10 +75,10 @@
Registry
- if project_nav_tab? :issues
- = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
+ = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
%span
- - if @project.default_issues_tracker?
+ - if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(IssuesFinder.new(current_user, project_id: @project.id).execute.opened.count)
Issues
@@ -113,7 +113,7 @@
Milestones
- if project_nav_tab? :merge_requests
- = nav_link(controller: @project.default_issues_tracker? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
+ = nav_link(controller: @project.issues_enabled? ? :merge_requests : [:merge_requests, :labels, :milestones]) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
%span.badge.count.merge_counter.js-merge-counter= number_with_delimiter(MergeRequestsFinder.new(current_user, project_id: @project.id).execute.opened.count)
diff --git a/app/views/layouts/nav/_project.html.haml b/app/views/layouts/nav/_project.html.haml
index fb90bb4b472..924cd2e9681 100644
--- a/app/views/layouts/nav/_project.html.haml
+++ b/app/views/layouts/nav/_project.html.haml
@@ -23,16 +23,16 @@
Registry
- if project_nav_tab? :issues
- = nav_link(controller: @project.default_issues_tracker? ? [:issues, :labels, :milestones, :boards] : :issues) do
+ = nav_link(controller: @project.issues_enabled? ? [:issues, :labels, :milestones, :boards] : :issues) do
= link_to project_issues_path(@project), title: 'Issues', class: 'shortcuts-issues' do
%span
Issues
- - if @project.default_issues_tracker?
+ - if @project.issues_enabled?
%span.badge.count.issue_counter= number_with_delimiter(issuables_count_for_state(:issues, :opened, finder: IssuesFinder.new(current_user, project_id: @project.id)))
- if project_nav_tab? :merge_requests
- controllers = [:merge_requests, 'projects/merge_requests/conflicts']
- - controllers.push(:merge_requests, :labels, :milestones) unless @project.default_issues_tracker?
+ - controllers.push(:merge_requests, :labels, :milestones) unless @project.issues_enabled?
= nav_link(controller: controllers) do
= link_to project_merge_requests_path(@project), title: 'Merge Requests', class: 'shortcuts-merge_requests' do
%span
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index f11afe8fc22..c7359d873d9 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -21,8 +21,8 @@
.commit
= author_avatar(commit, size: 36)
.commit-row-title
- %strong
- = link_to_gfm truncate(commit.title, length: 35), project_commit_path(@project, commit.id), class: "cdark"
+ %span.item-title.str-truncated-100
+ = link_to_gfm commit.title, project_commit_path(@project, commit.id), class: "cdark", title: commit.title
.pull-right
= link_to commit.short_id, project_commit_path(@project, commit), class: "commit-sha"
&nbsp;
diff --git a/app/views/projects/blob/viewers/_image.html.haml b/app/views/projects/blob/viewers/_image.html.haml
index 640d59b3174..1650aa8197f 100644
--- a/app/views/projects/blob/viewers/_image.html.haml
+++ b/app/views/projects/blob/viewers/_image.html.haml
@@ -1,2 +1,2 @@
.file-content.image_file
- %img{ src: blob_raw_url, alt: viewer.blob.name }
+ %img{ 'data-src': blob_raw_url, alt: viewer.blob.name }
diff --git a/app/views/projects/buttons/_star.html.haml b/app/views/projects/buttons/_star.html.haml
index e248676be0d..c82ae35a685 100644
--- a/app/views/projects/buttons/_star.html.haml
+++ b/app/views/projects/buttons/_star.html.haml
@@ -2,7 +2,7 @@
= link_to toggle_star_project_path(@project), { class: 'btn star-btn toggle-star', method: :post, remote: true } do
- if current_user.starred?(@project)
= icon('star')
- %span.starred= _('Unstar')
+ %span.starred= _('Unstar')
- else
= icon('star-o')
%span= s_('StarProject|Star')
diff --git a/app/views/projects/diffs/viewers/_image.html.haml b/app/views/projects/diffs/viewers/_image.html.haml
index 33d3dcbeafa..05877ceed3d 100644
--- a/app/views/projects/diffs/viewers/_image.html.haml
+++ b/app/views/projects/diffs/viewers/_image.html.haml
@@ -8,7 +8,7 @@
.image
%span.wrap
.frame{ class: (diff_file.deleted_file? ? 'deleted' : 'added') }
- %img{ src: blob_raw_path, alt: diff_file.file_path }
+ %img{ 'data-src': blob_raw_path, alt: diff_file.file_path }
%p.image-info= number_to_human_size(blob.size)
- else
.image
@@ -16,7 +16,7 @@
%span.wrap
.frame.deleted
%a{ href: project_blob_path(@project, tree_join(diff_file.old_content_sha, diff_file.old_path)) }
- %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
%p.image-info.hide
%span.meta-filesize= number_to_human_size(old_blob.size)
|
@@ -28,7 +28,7 @@
%span.wrap
.frame.added
%a{ href: project_blob_path(@project, tree_join(diff_file.content_sha, diff_file.new_path)) }
- %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%p.image-info.hide
%span.meta-filesize= number_to_human_size(blob.size)
|
@@ -41,10 +41,10 @@
.swipe.view.hide
.swipe-frame
.frame.deleted
- %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.swipe-wrap
.frame.added
- %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
%span.swipe-bar
%span.top-handle
%span.bottom-handle
@@ -52,9 +52,9 @@
.onion-skin.view.hide
.onion-skin-frame
.frame.deleted
- %img{ src: old_blob_raw_path, alt: diff_file.old_path }
+ %img{ 'data-src': old_blob_raw_path, alt: diff_file.old_path }
.frame.added
- %img{ src: blob_raw_path, alt: diff_file.new_path }
+ %img{ 'data-src': blob_raw_path, alt: diff_file.new_path }
.controls
.transparent
.drag-track
diff --git a/app/views/projects/merge_requests/_how_to_merge.html.haml b/app/views/projects/merge_requests/_how_to_merge.html.haml
index 766cb272bec..917ec7fdbda 100644
--- a/app/views/projects/merge_requests/_how_to_merge.html.haml
+++ b/app/views/projects/merge_requests/_how_to_merge.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('how_to_merge')
+
#modal_merge_info.modal
.modal-dialog
.modal-content
@@ -50,14 +53,3 @@
= succeed '.' do
You can also checkout merge requests locally by
= link_to 'following these guidelines', help_page_path('user/project/merge_requests/index.md', anchor: "checkout-merge-requests-locally"), target: '_blank', rel: 'noopener noreferrer'
-
-:javascript
- $(function(){
- var modal = $('#modal_merge_info').modal({modal: true, show:false});
- $('.how_to_merge_link').bind("click", function(){
- modal.show();
- });
- $('.modal-header .close').bind("click", function(){
- modal.hide();
- })
- })
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index bfeb746ee83..c020e7db380 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -4,7 +4,7 @@
- new_merge_request_path = project_new_merge_request_path(merge_project) if merge_project
- page_title "Merge Requests"
-- unless @project.default_issues_tracker?
+- unless @project.issues_enabled?
= content_for :sub_nav do
= render "projects/merge_requests/head"
diff --git a/app/views/projects/milestones/index.html.haml b/app/views/projects/milestones/index.html.haml
index a89387bc8f1..e0b29b0c2e1 100644
--- a/app/views/projects/milestones/index.html.haml
+++ b/app/views/projects/milestones/index.html.haml
@@ -1,7 +1,7 @@
- @no_container = true
- page_title 'Milestones'
-- if show_new_nav?
+- if show_new_nav? && can?(current_user, :admin_milestone, @project)
- content_for :breadcrumbs_extra do
= link_to "New milestone", new_namespace_project_milestone_path(@project.namespace, @project), class: 'btn btn-new', title: 'New milestone'
@@ -11,10 +11,10 @@
.top-area
= render 'shared/milestones_filter', counts: milestone_counts(@project.milestones)
- .nav-controls
+ .nav-controls{ class: ("nav-controls-new-nav" if show_new_nav?) }
= render 'shared/milestones_sort_dropdown'
- if can?(current_user, :admin_milestone, @project)
- = link_to new_project_milestone_path(@project), class: 'btn btn-new', title: 'New milestone' do
+ = link_to new_project_milestone_path(@project), class: "btn btn-new #{("visible-xs" if show_new_nav?)}", title: 'New milestone' do
New milestone
.milestones
diff --git a/app/views/shared/_mr_head.html.haml b/app/views/shared/_mr_head.html.haml
index 4211ec6351d..e7355ae2eea 100644
--- a/app/views/shared/_mr_head.html.haml
+++ b/app/views/shared/_mr_head.html.haml
@@ -1,4 +1,4 @@
-- if @project.default_issues_tracker?
+- if @project.issues_enabled?
= render "projects/issues/head"
- else
= render "projects/merge_requests/head"
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index c1acee1a211..5f3cdaefd54 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,6 +1,6 @@
- if @projects.any?
.project-item-select-holder
- = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at' }, with_feature_enabled: local_assigns[:with_feature_enabled]
- %a.btn.btn-new.new-project-item-select-button{ data: { relative_path: local_assigns[:path] } }
+ = project_select_tag :project_path, class: "project-item-select", data: { include_groups: local_assigns[:include_groups], order_by: 'last_activity_at', relative_path: local_assigns[:path] }, with_feature_enabled: local_assigns[:with_feature_enabled]
+ %a.btn.btn-new.new-project-item-select-button
= local_assigns[:label]
= icon('caret-down')
diff --git a/app/views/shared/icons/_icon_clone.svg b/app/views/shared/icons/_icon_clone.svg
new file mode 100644
index 00000000000..ccc897aa98f
--- /dev/null
+++ b/app/views/shared/icons/_icon_clone.svg
@@ -0,0 +1,3 @@
+<svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="14" height="14" viewBox="0 0 14 14">
+<path d="M13 12.75v-8.5q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h8.5q0.102 0 0.176-0.074t0.074-0.176zM14 4.25v8.5q0 0.516-0.367 0.883t-0.883 0.367h-8.5q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883zM11 1.25v1.25h-1v-1.25q0-0.102-0.074-0.176t-0.176-0.074h-8.5q-0.102 0-0.176 0.074t-0.074 0.176v8.5q0 0.102 0.074 0.176t0.176 0.074h1.25v1h-1.25q-0.516 0-0.883-0.367t-0.367-0.883v-8.5q0-0.516 0.367-0.883t0.883-0.367h8.5q0.516 0 0.883 0.367t0.367 0.883z"></path>
+</svg>
diff --git a/app/workers/pipeline_schedule_worker.rb b/app/workers/pipeline_schedule_worker.rb
index 7b485b3363c..d7087f20dfc 100644
--- a/app/workers/pipeline_schedule_worker.rb
+++ b/app/workers/pipeline_schedule_worker.rb
@@ -6,15 +6,12 @@ class PipelineScheduleWorker
Ci::PipelineSchedule.active.where("next_run_at < ?", Time.now)
.preload(:owner, :project).find_each do |schedule|
begin
- unless schedule.runnable_by_owner?
- schedule.deactivate!
- next
- end
-
- Ci::CreatePipelineService.new(schedule.project,
- schedule.owner,
- ref: schedule.ref)
+ pipeline = Ci::CreatePipelineService.new(schedule.project,
+ schedule.owner,
+ ref: schedule.ref)
.execute(:schedule, save_on_errors: false, schedule: schedule)
+
+ schedule.deactivate! unless pipeline.persisted?
rescue => e
Rails.logger.error "#{schedule.id}: Failed to create a scheduled pipeline: #{e.message}"
ensure
diff --git a/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml
new file mode 100644
index 00000000000..837a34bd067
--- /dev/null
+++ b/changelogs/unreleased/22600-related-resources-uris-using-grape-source-helpers.yml
@@ -0,0 +1,4 @@
+---
+title: Declare related resources into V4 API entities
+merge_request:
+author:
diff --git a/changelogs/unreleased/26372-duplicate-issue-slash-command.yml b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml
new file mode 100644
index 00000000000..3108344e0bf
--- /dev/null
+++ b/changelogs/unreleased/26372-duplicate-issue-slash-command.yml
@@ -0,0 +1,4 @@
+---
+title: Added /duplicate quick action to close a duplicate issue
+merge_request: 12845
+author: Ryan Scott
diff --git a/changelogs/unreleased/28202_decrease_abc_threshold_step2.yml b/changelogs/unreleased/28202_decrease_abc_threshold_step2.yml
new file mode 100644
index 00000000000..b8f30b52b18
--- /dev/null
+++ b/changelogs/unreleased/28202_decrease_abc_threshold_step2.yml
@@ -0,0 +1,4 @@
+---
+title: Decrease ABC threshold to 56.96
+merge_request: 11227
+author: Maxim Rydkin
diff --git a/changelogs/unreleased/2971-multiproject-grah-ce-port.yml b/changelogs/unreleased/2971-multiproject-grah-ce-port.yml
new file mode 100644
index 00000000000..37584cac6ab
--- /dev/null
+++ b/changelogs/unreleased/2971-multiproject-grah-ce-port.yml
@@ -0,0 +1,4 @@
+---
+title: Fix vertical alignment in firefox and safari for pipeline mini graph
+merge_request:
+author:
diff --git a/changelogs/unreleased/30634-protected-pipeline.yml b/changelogs/unreleased/30634-protected-pipeline.yml
new file mode 100644
index 00000000000..e46538e5b46
--- /dev/null
+++ b/changelogs/unreleased/30634-protected-pipeline.yml
@@ -0,0 +1,5 @@
+---
+title: Disallow running the pipeline if ref is protected and user cannot merge the
+ branch or create the tag
+merge_request: 11910
+author:
diff --git a/changelogs/unreleased/31129-jira-project-key-elim.yml b/changelogs/unreleased/31129-jira-project-key-elim.yml
new file mode 100644
index 00000000000..bfa0e99f250
--- /dev/null
+++ b/changelogs/unreleased/31129-jira-project-key-elim.yml
@@ -0,0 +1,4 @@
+---
+title: Remove project_key from the Jira configuration
+merge_request: 12050
+author:
diff --git a/changelogs/unreleased/31533-usage-data-projects-stats.yml b/changelogs/unreleased/31533-usage-data-projects-stats.yml
new file mode 100644
index 00000000000..11bb6118337
--- /dev/null
+++ b/changelogs/unreleased/31533-usage-data-projects-stats.yml
@@ -0,0 +1,4 @@
+---
+title: Add Slack and JIRA services counts to Usage Data
+merge_request:
+author:
diff --git a/changelogs/unreleased/33097-issue-tracker.yml b/changelogs/unreleased/33097-issue-tracker.yml
new file mode 100644
index 00000000000..0b13f7165db
--- /dev/null
+++ b/changelogs/unreleased/33097-issue-tracker.yml
@@ -0,0 +1,4 @@
+---
+title: Associate Issues tab only with internal issues tracker
+merge_request:
+author:
diff --git a/changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml b/changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml
new file mode 100644
index 00000000000..1911705dd2b
--- /dev/null
+++ b/changelogs/unreleased/34110-memory-usage-notice-doesn-t-link-anywhere.yml
@@ -0,0 +1,4 @@
+---
+title: Added link to the MR widget that directs to the monitoring dashboard
+merge_request:
+author:
diff --git a/changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml b/changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml
new file mode 100644
index 00000000000..d188a558d38
--- /dev/null
+++ b/changelogs/unreleased/34361-lazy-load-images-on-the-frontend.yml
@@ -0,0 +1,4 @@
+---
+title: Lazy load images for better Frontend performance
+merge_request: 12503
+author:
diff --git a/changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml b/changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml
new file mode 100644
index 00000000000..4fd60a79782
--- /dev/null
+++ b/changelogs/unreleased/35163-url-in-commit-message-can-be-broken-in-blame.yml
@@ -0,0 +1,4 @@
+---
+title: Use only CSS to truncate commit message in blame
+merge_request: 12900
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml b/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml
new file mode 100644
index 00000000000..45b6c57579b
--- /dev/null
+++ b/changelogs/unreleased/35204-doc-api-ci-lint-typo.yml
@@ -0,0 +1,4 @@
+---
+title: Add link to doc/api/ci/lint.md
+merge_request: 12914
+author: Takuya Noguchi
diff --git a/changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml b/changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml
new file mode 100644
index 00000000000..73808030f4c
--- /dev/null
+++ b/changelogs/unreleased/35338-deploy-keys-should-not-show-pending-delete-projects.yml
@@ -0,0 +1,4 @@
+---
+title: Pending delete projects should not show in deploy keys.
+merge_request: 13088
+author:
diff --git a/changelogs/unreleased/35391-fix-star-i18n-in-js.yml b/changelogs/unreleased/35391-fix-star-i18n-in-js.yml
new file mode 100644
index 00000000000..a6fd4dc89fd
--- /dev/null
+++ b/changelogs/unreleased/35391-fix-star-i18n-in-js.yml
@@ -0,0 +1,4 @@
+---
+title: Fix translations for Star/Unstar in JS file
+merge_request:
+author:
diff --git a/changelogs/unreleased/35399-mini-graph-commits-box.yml b/changelogs/unreleased/35399-mini-graph-commits-box.yml
deleted file mode 100644
index ed080ed86b4..00000000000
--- a/changelogs/unreleased/35399-mini-graph-commits-box.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix margins in the mini graph for pipeline in commits box
-merge_request:
-author:
diff --git a/changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml b/changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml
new file mode 100644
index 00000000000..fa906accbb8
--- /dev/null
+++ b/changelogs/unreleased/35453-pending-delete-projects-error-in-admin-dashboard-fix.yml
@@ -0,0 +1,4 @@
+---
+title: Fixes 500 error caused by pending delete projects in admin dashboard
+merge_request: 13067
+author:
diff --git a/changelogs/unreleased/35478-allow-admin-to-read-user-list.yml b/changelogs/unreleased/35478-allow-admin-to-read-user-list.yml
new file mode 100644
index 00000000000..da4b730f0ca
--- /dev/null
+++ b/changelogs/unreleased/35478-allow-admin-to-read-user-list.yml
@@ -0,0 +1,4 @@
+---
+title: Allow admin to read_users_list even if it's restricted
+merge_request: 13066
+author:
diff --git a/changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml b/changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml
new file mode 100644
index 00000000000..b5cf521561a
--- /dev/null
+++ b/changelogs/unreleased/add-instrumentation-to-link-to-gfm.yml
@@ -0,0 +1,4 @@
+---
+title: Add instrumentation to MarkupHelper#link_to_gfm
+merge_request: 13069
+author:
diff --git a/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml b/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml
new file mode 100644
index 00000000000..42e1c9e8f83
--- /dev/null
+++ b/changelogs/unreleased/bump-omniauth-ldap-gem-version.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent LDAP login callback from being called with a GET request
+merge_request: 13059
+author:
diff --git a/changelogs/unreleased/bvl-free-unused-names.yml b/changelogs/unreleased/bvl-free-unused-names.yml
new file mode 100644
index 00000000000..53acb95e5bb
--- /dev/null
+++ b/changelogs/unreleased/bvl-free-unused-names.yml
@@ -0,0 +1,5 @@
+---
+title: Free up some top level words, reject top level groups named like files in the
+ public folder
+merge_request: 12932
+author:
diff --git a/changelogs/unreleased/dz-fix-calendar-today.yml b/changelogs/unreleased/dz-fix-calendar-today.yml
new file mode 100644
index 00000000000..5320d8b26b5
--- /dev/null
+++ b/changelogs/unreleased/dz-fix-calendar-today.yml
@@ -0,0 +1,4 @@
+---
+title: Fix today day highlight in calendar
+merge_request: 13048
+author:
diff --git a/changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml b/changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml
new file mode 100644
index 00000000000..7a68e91c6d3
--- /dev/null
+++ b/changelogs/unreleased/fix-gb-fix-build-merge-request-link-to-fork-project.yml
@@ -0,0 +1,4 @@
+---
+title: Fix job merge request link to a forked source project
+merge_request: 12965
+author:
diff --git a/changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml b/changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml
deleted file mode 100644
index 334d8ca4d9e..00000000000
--- a/changelogs/unreleased/fix-sm-32790-pipeline_schedules-pages-throwing-error-500.yml
+++ /dev/null
@@ -1,4 +0,0 @@
----
-title: Fix pipeline_schedules pages throwing error 500 (when ref is empty)
-merge_request: 12983
-author:
diff --git a/changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml b/changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml
new file mode 100644
index 00000000000..37a5fa66d13
--- /dev/null
+++ b/changelogs/unreleased/mk-add-lower-path-index-to-redirect-routes.yml
@@ -0,0 +1,4 @@
+---
+title: Improve redirect route query performance
+merge_request: 13062
+author:
diff --git a/changelogs/unreleased/new-navigation-custom-logo.yml b/changelogs/unreleased/new-navigation-custom-logo.yml
new file mode 100644
index 00000000000..22e6c5dc7e5
--- /dev/null
+++ b/changelogs/unreleased/new-navigation-custom-logo.yml
@@ -0,0 +1,4 @@
+---
+title: Fix sizing of custom header logo in new navigation
+merge_request:
+author:
diff --git a/changelogs/unreleased/post-upload-pack-opt-out.yml b/changelogs/unreleased/post-upload-pack-opt-out.yml
new file mode 100644
index 00000000000..302a99795a0
--- /dev/null
+++ b/changelogs/unreleased/post-upload-pack-opt-out.yml
@@ -0,0 +1,4 @@
+---
+title: Enable gitaly_post_upload_pack by default
+merge_request: 13078
+author:
diff --git a/config/initializers/8_metrics.rb b/config/initializers/8_metrics.rb
index 25630b298ce..2aeb94d47cd 100644
--- a/config/initializers/8_metrics.rb
+++ b/config/initializers/8_metrics.rb
@@ -114,6 +114,9 @@ def instrument_classes(instrumentation)
# This is a Rails scope so we have to instrument it manually.
instrumentation.instrument_method(Project, :visible_to_user)
+ # Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/34509
+ instrumentation.instrument_method(MarkupHelper, :link_to_gfm)
+
# Needed for https://gitlab.com/gitlab-org/gitlab-ce/issues/30224#note_32306159
instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits)
end
diff --git a/config/initializers/grape_route_helpers_fix.rb b/config/initializers/grape_route_helpers_fix.rb
new file mode 100644
index 00000000000..d3cf9e453d0
--- /dev/null
+++ b/config/initializers/grape_route_helpers_fix.rb
@@ -0,0 +1,35 @@
+if defined?(GrapeRouteHelpers)
+ module GrapeRouteHelpers
+ class DecoratedRoute
+ # GrapeRouteHelpers gem tries to parse the versions
+ # from a string, not supporting Grape `version` array definition.
+ #
+ # Without the following fix, we get this on route helpers generation:
+ #
+ # => undefined method `scan' for ["v3", "v4"]
+ #
+ # 2.0.0 implementation of this method:
+ #
+ # ```
+ # def route_versions
+ # version_pattern = /[^\[",\]\s]+/
+ # if route_version
+ # route_version.scan(version_pattern)
+ # else
+ # [nil]
+ # end
+ # end
+ # ```
+ def route_versions
+ return [nil] if route_version.nil? || route_version.empty?
+
+ if route_version.is_a?(String)
+ version_pattern = /[^\[",\]\s]+/
+ route_version.scan(version_pattern)
+ else
+ route_version
+ end
+ end
+ end
+ end
+end
diff --git a/config/routes/api.rb b/config/routes/api.rb
index 69c8efc151c..ce7a7c88900 100644
--- a/config/routes/api.rb
+++ b/config/routes/api.rb
@@ -1,2 +1,2 @@
API::API.logger Rails.logger
-mount API::API => '/api'
+mount API::API => '/'
diff --git a/config/webpack.config.js b/config/webpack.config.js
index a7d92bc53b7..f08daa2fddb 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -42,6 +42,7 @@ var config = {
group: './group.js',
groups: './groups/index.js',
groups_list: './groups_list.js',
+ how_to_merge: './how_to_merge.js',
issue_show: './issue_show/index.js',
integrations: './integrations',
job_details: './jobs/job_details_bundle.js',
diff --git a/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb
new file mode 100644
index 00000000000..db60c2087b9
--- /dev/null
+++ b/db/migrate/20170724214302_add_lower_path_index_to_redirect_routes.rb
@@ -0,0 +1,34 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddLowerPathIndexToRedirectRoutes < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+ INDEX_NAME = 'index_on_redirect_routes_lower_path'
+
+ disable_ddl_transaction!
+
+ def up
+ return unless Gitlab::Database.postgresql?
+
+ execute "CREATE INDEX CONCURRENTLY #{INDEX_NAME} ON redirect_routes (LOWER(path));"
+ end
+
+ def down
+ return unless Gitlab::Database.postgresql?
+
+ # Why not use remove_concurrent_index_by_name?
+ #
+ # `index_exists?` doesn't work on this index. Perhaps this is related to the
+ # fact that the index doesn't show up in the schema. And apparently it isn't
+ # trivial to write a query that checks for an index. BUT there is a
+ # convenient `IF EXISTS` parameter for `DROP INDEX`.
+ if supports_drop_index_concurrently?
+ disable_statement_timeout
+ execute "DROP INDEX CONCURRENTLY IF EXISTS #{INDEX_NAME};"
+ else
+ execute "DROP INDEX IF EXISTS #{INDEX_NAME};"
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 567ba4d061b..5e3237bf4e4 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: 20170720111708) do
+ActiveRecord::Schema.define(version: 20170724214302) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/README.md b/doc/README.md
index 2b3b0998fcc..ac7311a8c13 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -11,15 +11,11 @@ self-hosted, free to use. Every feature available in GitLab CE is also available
self-hosted, fully featured solution of GitLab, available under distinct [subscriptions](https://about.gitlab.com/products/): **GitLab Enterprise Edition Starter (EES)** and **GitLab Enterprise Edition Premium (EEP)**.
- **GitLab.com**: SaaS GitLab solution, with [free and paid subscriptions](https://about.gitlab.com/gitlab-com/). GitLab.com is hosted by GitLab, Inc., and administrated by GitLab (users don't have access to admin settings).
-**GitLab EE** contains all features available in **GitLab CE**,
+> **GitLab EE** contains all features available in **GitLab CE**,
plus premium features available in each version: **Enterprise Edition Starter**
(**EES**) and **Enterprise Edition Premium** (**EEP**). Everything available in
**EES** is also available in **EEP**.
-**Note:** _We are unifying the documentation for CE and EE. To check if certain feature is
-available in CE or EE, look for a note right below the page title containing the GitLab
-version which introduced that feature._
-
----
Shortcuts to GitLab's most visited docs:
@@ -40,6 +36,7 @@ Shortcuts to GitLab's most visited docs:
### User account
+- [User documentation](user/index.md)
- [Authentication](topics/authentication/index.md): Account security with two-factor authentication, setup your ssh keys and deploy keys for secure access to your projects.
- [Profile settings](profile/README.md): Manage your profile settings, two factor authentication and more.
- [User permissions](user/permissions.md): Learn what each role in a project (external/guest/reporter/developer/master/owner) can do.
diff --git a/doc/administration/container_registry.md b/doc/administration/container_registry.md
index afafb6bf1f5..8cb0e5b1562 100644
--- a/doc/administration/container_registry.md
+++ b/doc/administration/container_registry.md
@@ -465,23 +465,42 @@ on how to achieve that.
## Disable Container Registry but use GitLab as an auth endpoint
-You can disable the embedded Container Registry to use an external one, but
-still use GitLab as an auth endpoint.
-
**Omnibus GitLab**
+
+You can use GitLab as an auth endpoint and use a non-bundled Container Registry.
+
1. Open `/etc/gitlab/gitlab.rb` and set necessary configurations:
```ruby
- registry['enable'] = false
gitlab_rails['registry_enabled'] = true
gitlab_rails['registry_host'] = "registry.gitlab.example.com"
gitlab_rails['registry_port'] = "5005"
gitlab_rails['registry_api_url'] = "http://localhost:5000"
- gitlab_rails['registry_key_path'] = "/var/opt/gitlab/gitlab-rails/certificate.key"
gitlab_rails['registry_path'] = "/var/opt/gitlab/gitlab-rails/shared/registry"
gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer"
```
+1. A certificate keypair is required for GitLab and the Container Registry to
+ communicate securely. By default omnibus-gitlab will generate one keypair,
+ which is saved to `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key`.
+ When using an non-bundled Container Registry, you will need to supply a
+ custom certificate key. To do that, add the following to
+ `/etc/gitlab/gitlab.rb`
+
+ ```ruby
+ gitlab_rails['registry_key_path'] = "/custom/path/to/registry-key.key"
+ # registry['internal_key'] should contain the contents of the custom key
+ # file. Line breaks in the key file should be marked using `\n` character
+ # Example:
+ registry['internal_key'] = "---BEGIN RSA PRIVATE KEY---\nMIIEpQIBAA\n"
+ ```
+
+ **Note:** The file specified at `registry_key_path` gets populated with the
+ content specified by `internal_key`, each time reconfigure is executed. If
+ no file is specified, omnibus-gitlab will default it to
+ `/var/opt/gitlab/gitlab-rails/etc/gitlab-registry.key` and will populate
+ it.
+
1. Save the file and [reconfigure GitLab][] for the changes to take effect.
**Installations from source**
diff --git a/doc/administration/high_availability/database.md b/doc/administration/high_availability/database.md
index da9687aa849..ca6d8d2de67 100644
--- a/doc/administration/high_availability/database.md
+++ b/doc/administration/high_availability/database.md
@@ -97,9 +97,12 @@ If you use a cloud-managed service, or provide your own PostgreSQL:
Enter new password:
Enter it again:
```
-
-1. Enable the `pg_trgm` extension:
+1. Exit from editing `template1` prompt by typing `\q` and Enter.
+1. Enable the `pg_trgm` extension within the `gitlabhq_production` database:
+
```
+ gitlab-psql -d gitlabhq_production
+
CREATE EXTENSION pg_trgm;
# Output:
diff --git a/doc/api/README.md b/doc/api/README.md
index 95e7a457848..a888c0ebb4e 100644
--- a/doc/api/README.md
+++ b/doc/api/README.md
@@ -29,7 +29,8 @@ following locations:
- [Keys](keys.md)
- [Labels](labels.md)
- [Merge Requests](merge_requests.md)
-- [Milestones](milestones.md)
+- [Project milestones](milestones.md)
+- [Group milestones](group_milestones.md)
- [Namespaces](namespaces.md)
- [Notes](notes.md) (comments)
- [Notification settings](notification_settings.md)
diff --git a/doc/api/ci/lint.md b/doc/api/ci/lint.md
index 6a4dca92cfe..e4a6dc809b1 100644
--- a/doc/api/ci/lint.md
+++ b/doc/api/ci/lint.md
@@ -47,3 +47,5 @@ Example responses:
"error": "content is missing"
}
```
+
+[ce-5953]: https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/5953
diff --git a/doc/api/group_milestones.md b/doc/api/group_milestones.md
new file mode 100644
index 00000000000..086fba7e91d
--- /dev/null
+++ b/doc/api/group_milestones.md
@@ -0,0 +1,120 @@
+# Group milestones API
+
+## List group milestones
+
+Returns a list of group milestones.
+
+```
+GET /groups/:id/milestones
+GET /groups/:id/milestones?iids=42
+GET /groups/:id/milestones?iids[]=42&iids[]=43
+GET /groups/:id/milestones?state=active
+GET /groups/:id/milestones?state=closed
+GET /groups/:id/milestones?search=version
+```
+
+Parameters:
+
+| Attribute | Type | Required | Description |
+| --------- | ---- | -------- | ----------- |
+| `id` | integer/string | yes | The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `iids` | Array[integer] | optional | Return only the milestones having the given `iids` |
+| `state` | string | optional | Return only `active` or `closed` milestones` |
+| `search` | string | optional | Return only milestones with a title or description matching the provided string |
+
+```bash
+curl --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/groups/5/milestones
+```
+
+Example Response:
+
+```json
+[
+ {
+ "id": 12,
+ "iid": 3,
+ "group_id": 16,
+ "title": "10.0",
+ "description": "Version",
+ "due_date": "2013-11-29",
+ "start_date": "2013-11-10",
+ "state": "active",
+ "updated_at": "2013-10-02T09:24:18Z",
+ "created_at": "2013-10-02T09:24:18Z"
+ }
+]
+```
+
+
+## Get single milestone
+
+Gets a single group milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of the group milestone
+
+## Create new milestone
+
+Creates a new group milestone.
+
+```
+POST /groups/:id/milestones
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `title` (required) - The title of an milestone
+- `description` (optional) - The description of the milestone
+- `due_date` (optional) - The due date of the milestone
+- `start_date` (optional) - The start date of the milestone
+
+## Edit milestone
+
+Updates an existing group milestone.
+
+```
+PUT /groups/:id/milestones/:milestone_id
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
+- `title` (optional) - The title of a milestone
+- `description` (optional) - The description of a milestone
+- `due_date` (optional) - The due date of the milestone
+- `start_date` (optional) - The start date of the milestone
+- `state_event` (optional) - The state event of the milestone (close|activate)
+
+## Get all issues assigned to a single milestone
+
+Gets all issues assigned to a single group milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id/issues
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
+
+## Get all merge requests assigned to a single milestone
+
+Gets all merge requests assigned to a single group milestone.
+
+```
+GET /groups/:id/milestones/:milestone_id/merge_requests
+```
+
+Parameters:
+
+- `id` (required) - The ID or [URL-encoded path of the group](README.md#namespaced-path-encoding) owned by the authenticated user
+- `milestone_id` (required) - The ID of a group milestone
diff --git a/doc/api/issues.md b/doc/api/issues.md
index a00a63bad4b..0e391c75cd3 100644
--- a/doc/api/issues.md
+++ b/doc/api/issues.md
@@ -356,7 +356,13 @@ Example response:
"user_notes_count": 1,
"due_date": null,
"web_url": "http://example.com/example/example/issues/1",
- "confidential": false
+ "confidential": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects/1/issues/2",
+ "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+ "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+ "project": "http://example.com/api/v4/projects/1"
+ }
}
```
@@ -418,7 +424,13 @@ Example response:
"user_notes_count": 0,
"due_date": null,
"web_url": "http://example.com/example/example/issues/14",
- "confidential": false
+ "confidential": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects/1/issues/2",
+ "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+ "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+ "project": "http://example.com/api/v4/projects/1"
+ }
}
```
@@ -481,7 +493,13 @@ Example response:
"user_notes_count": 0,
"due_date": "2016-07-22",
"web_url": "http://example.com/example/example/issues/15",
- "confidential": false
+ "confidential": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects/1/issues/2",
+ "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+ "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+ "project": "http://example.com/api/v4/projects/1"
+ }
}
```
@@ -567,7 +585,13 @@ Example response:
},
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
- "confidential": false
+ "confidential": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects/1/issues/2",
+ "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+ "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+ "project": "http://example.com/api/v4/projects/1"
+ }
}
```
@@ -632,7 +656,13 @@ Example response:
},
"due_date": null,
"web_url": "http://example.com/example/example/issues/11",
- "confidential": false
+ "confidential": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects/1/issues/2",
+ "notes": "http://example.com/api/v4/projects/1/issues/2/notes",
+ "award_emoji": "http://example.com/api/v4/projects/1/issues/2/award_emoji",
+ "project": "http://example.com/api/v4/projects/1"
+ }
}
```
diff --git a/doc/api/projects.md b/doc/api/projects.md
index 61ae89a64c0..d3f8e509612 100644
--- a/doc/api/projects.md
+++ b/doc/api/projects.md
@@ -99,7 +99,16 @@ Parameters:
"repository_size": 1038090,
"lfs_objects_size": 0,
"job_artifacts_size": 0
- }
+ },
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ },
},
{
"id": 6,
@@ -168,6 +177,15 @@ Parameters:
"repository_size": 2066080,
"lfs_objects_size": 0,
"job_artifacts_size": 0
+ },
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
}
}
]
@@ -257,6 +275,15 @@ Parameters:
"repository_size": 1038090,
"lfs_objects_size": 0,
"job_artifacts_size": 0
+ },
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
}
},
{
@@ -326,6 +353,15 @@ Parameters:
"repository_size": 2066080,
"lfs_objects_size": 0,
"job_artifacts_size": 0
+ },
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
}
}
]
@@ -427,6 +463,15 @@ Parameters:
"repository_size": 1038090,
"lfs_objects_size": 0,
"job_artifacts_size": 0
+ },
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
}
}
```
@@ -659,7 +704,16 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ }
}
```
@@ -725,7 +779,16 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ }
}
```
@@ -809,7 +872,16 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ }
}
```
@@ -893,7 +965,16 @@ Example response:
"shared_with_groups": [],
"only_allow_merge_if_pipeline_succeeds": false,
"only_allow_merge_if_all_discussions_are_resolved": false,
- "request_access_enabled": false
+ "request_access_enabled": false,
+ "_links": {
+ "self": "http://example.com/api/v4/projects",
+ "issues": "http://example.com/api/v4/projects/1/issues",
+ "merge_requests": "http://example.com/api/v4/projects/1/merge_requests",
+ "repo_branches": "http://example.com/api/v4/projects/1/repository_branches",
+ "labels": "http://example.com/api/v4/projects/1/labels",
+ "events": "http://example.com/api/v4/projects/1/events",
+ "members": "http://example.com/api/v4/projects/1/members"
+ }
}
```
diff --git a/doc/api/v3_to_v4.md b/doc/api/v3_to_v4.md
index 9db8e0351cf..9835fab7c98 100644
--- a/doc/api/v3_to_v4.md
+++ b/doc/api/v3_to_v4.md
@@ -2,9 +2,11 @@
Since GitLab 9.0, API V4 is the preferred version to be used.
-API V3 will be removed in GitLab 9.5, to be released on August 22, 2017. In the
-meantime, we advise you to make any necessary changes to applications that use
-V3. The V3 API documentation is still [available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
+API V3 will be unsupported from GitLab 9.5, to be released on August
+22, 2017. It will be removed in GitLab 9.5 or later. In the meantime, we advise
+you to make any necessary changes to applications that use V3. The V3 API
+documentation is still
+[available](https://gitlab.com/gitlab-org/gitlab-ce/blob/8-16-stable/doc/api/README.md).
Below are the changes made between V3 and V4.
diff --git a/doc/articles/index.md b/doc/articles/index.md
index 342fa88e80f..a4e41517d83 100644
--- a/doc/articles/index.md
+++ b/doc/articles/index.md
@@ -11,6 +11,7 @@ They are written by members of the GitLab Team and by
- **LDAP**
- [How to configure LDAP with GitLab CE](how_to_configure_ldap_gitlab_ce/index.md)
+ - [How to configure LDAP with GitLab EE](https://docs.gitlab.com/ee/articles/how_to_configure_ldap_gitlab_ee/)
## Git
@@ -23,3 +24,23 @@ They are written by members of the GitLab Team and by
- [Part 2: Quick start guide - Setting up GitLab Pages](../user/project/pages/getting_started_part_two.md)
- [Part 3: Setting Up Custom Domains - DNS Records and SSL/TLS Certificates](../user/project/pages/getting_started_part_three.md)
- [Part 4: Creating and tweaking `.gitlab-ci.yml` for GitLab Pages](../user/project/pages/getting_started_part_four.md)
+- [Building a new GitLab Docs site with Nanoc, GitLab CI, and GitLab Pages](https://about.gitlab.com/2016/12/07/building-a-new-gitlab-docs-site-with-nanoc-gitlab-ci-and-gitlab-pages/)
+- [GitLab CI: Deployment & Environments](https://about.gitlab.com/2016/08/26/ci-deployment-and-environments/)
+
+## Sofware development
+
+- [In 13 minutes from Kubernetes to a complete application development tool](https://about.gitlab.com/2016/11/14/idea-to-production/)
+- [Making CI Easier with GitLab](https://about.gitlab.com/2017/07/13/making-ci-easier-with-gitlab/)
+- [Fast and Natural Continuous Integration with GitLab CI](https://about.gitlab.com/2017/05/22/fast-and-natural-continuous-integration-with-gitlab-ci/)
+- [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/)
+- [Continuous Integration, Delivery, and Deployment with GitLab](https://about.gitlab.com/2016/08/05/continuous-integration-delivery-and-deployment-with-gitlab/)
+
+## Build, test, and deploy with GitLab CI/CD
+
+**Build, test, and deploy** the software you develop with **[GitLab CI/CD](../ci/README.md)**
+
+- [Continuous Delivery of a Spring Boot application with GitLab CI and Kubernetes](https://about.gitlab.com/2016/12/14/continuous-delivery-of-a-spring-boot-application-with-gitlab-ci-and-kubernetes/)
+- [Automated Debian Package Build with GitLab CI](https://about.gitlab.com/2016/10/12/automated-debian-package-build-with-gitlab-ci/)
+- [Building an Elixir Release into a Docker image using GitLab CI](https://about.gitlab.com/2016/08/11/building-an-elixir-release-into-docker-image-using-gitlab-ci-part-1/)
+- [Setting up GitLab CI for Android projects](https://about.gitlab.com/2016/11/30/setting-up-gitlab-ci-for-android-projects/)
+- [How to use GitLab CI and MacStadium to build your macOS or iOS projects](https://about.gitlab.com/2017/05/15/how-to-use-macstadium-and-gitlab-ci-to-build-your-macos-or-ios-projects/)
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index 4ed89146072..e3f37616757 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -133,6 +133,55 @@ reviewee.
tomorrow. When you are not able to find the right balance, ask other people
about their opinion.
+### GitLab-specific concerns
+
+GitLab is used in a lot of places. Many users use
+our [Omnibus packages](https://about.gitlab.com/installation/), but some use
+the [Docker images](https://docs.gitlab.com/omnibus/docker/), some are
+[installed from source](https://docs.gitlab.com/ce/install/installation.html),
+and there are other installation methods available. GitLab.com itself is a large
+Enterprise Edition instance. This has some implications:
+
+1. **Query changes** should be tested to ensure that they don't result in worse
+ performance at the scale of GitLab.com:
+ 1. Generating large quantities of data locally can help.
+ 2. Asking for query plans from GitLab.com is the most reliable way to validate
+ these.
+2. **Database migrations** must be:
+ 1. Reversible.
+ 2. Performant at the scale of GitLab.com - ask a maintainer to test the
+ migration on the staging environment if you aren't sure.
+ 3. Categorised correctly:
+ - Regular migrations run before the new code is running on the instance.
+ - [Post-deployment migrations](post_deployment_migrations.md) run _after_
+ the new code is deployed, when the instance is configured to do that.
+ - [Background migrations](background_migrations.md) run in Sidekiq, and
+ should only be done for migrations that would take an extreme amount of
+ time at GitLab.com scale.
+3. **Sidekiq workers**
+ [cannot change in a backwards-incompatible way](sidekiq_style_guide.md#removing-or-renaming-queues):
+ 1. Sidekiq queues are not drained before a deploy happens, so there will be
+ workers in the queue from the previous version of GitLab.
+ 2. If you need to change a method signature, try to do so across two releases,
+ and accept both the old and new arguments in the first of those.
+ 3. Similarly, if you need to remove a worker, stop it from being scheduled in
+ one release, then remove it in the next. This will allow existing jobs to
+ execute.
+ 4. Don't forget, not every instance will upgrade to every intermediate version
+ (some people may go from X.1.0 to X.10.0, or even try bigger upgrades!), so
+ try to be liberal in accepting the old format if it is cheap to do so.
+4. **Cached values** may persist across releases. If you are changing the type a
+ cached value returns (say, from a string or nil to an array), change the
+ cache key at the same time.
+5. **Settings** should be added as a
+ [last resort](https://about.gitlab.com/handbook/product/#convention-over-configuration).
+ If you're adding a new setting in `gitlab.yml`:
+ 1. Try to avoid that, and add to `ApplicationSetting` instead.
+ 2. Ensure that it is also
+ [added to Omnibus](https://docs.gitlab.com/omnibus/settings/gitlab.yml.html#adding-a-new-setting-to-gitlab-yml).
+6. **Filesystem access** can be slow, so try to avoid
+ [shared files](shared_files.md) when an alternative solution is available.
+
### Credits
Largely based on the [thoughtbot code review guide].
diff --git a/doc/development/fe_guide/performance.md b/doc/development/fe_guide/performance.md
index 2ddcbe13afa..f25313d6cff 100644
--- a/doc/development/fe_guide/performance.md
+++ b/doc/development/fe_guide/performance.md
@@ -23,6 +23,18 @@ controlled by the server.
1. The backend code will most likely be using etags. You do not and should not check for status
`304 Not Modified`. The browser will transform it for you.
+### Lazy Loading
+
+To improve the time to first render we are using lazy loading for images. This works by setting
+the actual image source on the `data-src` attribute. After the HTML is rendered and JavaScript is loaded,
+the value of `data-src` will be moved to `src` automatically if the image is in the current viewport.
+
+* Prepare images in HTML for lazy loading by renaming the `src` attribute to `data-src`
+* If you are using the Rails `image_tag` helper, all images will be lazy-loaded by default unless `lazy: false` is provided.
+
+If you are asynchronously adding content which contains lazy images then you need to call the function
+`gl.lazyLoader.searchLazyImages()` which will search for lazy images and load them if needed.
+
## Reducing Asset Footprint
### Page-specific JavaScript
diff --git a/doc/development/fe_guide/style_guide_js.md b/doc/development/fe_guide/style_guide_js.md
index ae844fa1051..149a0159680 100644
--- a/doc/development/fe_guide/style_guide_js.md
+++ b/doc/development/fe_guide/style_guide_js.md
@@ -447,6 +447,7 @@ A forEach will cause side effects, it will be mutating the array being iterated.
1. `name`
1. `props`
1. `mixins`
+ 1. `directives`
1. `data`
1. `components`
1. `computedProps`
diff --git a/doc/integration/README.md b/doc/integration/README.md
index e56e58498a6..d70b9a7f54b 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -5,19 +5,23 @@ trackers and external authentication.
See the documentation below for details on how to configure these services.
-- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker
+- [Akismet](akismet.md) Configure Akismet to stop spam
+- [Auth0 OmniAuth](auth0.md) Enable the Auth0 OmniAuth provider
+- [Bitbucket](bitbucket.md) Import projects from Bitbucket.org and login to your GitLab instance with your
+Bitbucket.org account
+- [CAS](cas.md) Configure GitLab to sign in using CAS
- [External issue tracker](external-issue-tracker.md) Redmine, JIRA, etc.
+- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
+- [JIRA](../user/project/integrations/jira.md) Integrate with the JIRA issue tracker
+- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
- [LDAP](ldap.md) Set up sign in via LDAP
-- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
-- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
-- [CAS](cas.md) Configure GitLab to sign in using CAS
- [OAuth2 provider](oauth_provider.md) OAuth2 application creation
+- [OmniAuth](omniauth.md) Sign in via Twitter, GitHub, GitLab.com, Google, Bitbucket, Facebook, Shibboleth, SAML, Crowd, Azure and Authentiq ID
- [OpenID Connect](openid_connect_provider.md) Use GitLab as an identity provider
-- [Gmail actions buttons](gmail_action_buttons_for_gitlab.md) Adds GitLab actions to messages
-- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
-- [Akismet](akismet.md) Configure Akismet to stop spam
-- [Koding](../administration/integration/koding.md) Configure Koding to use IDE integration
- [PlantUML](../administration/integration/plantuml.md) Configure PlantUML to use diagrams in AsciiDoc documents.
+- [reCAPTCHA](recaptcha.md) Configure GitLab to use Google reCAPTCHA for new users
+- [SAML](saml.md) Configure GitLab as a SAML 2.0 Service Provider
+- [Trello](trello_power_up.md) Integrate Trello with GitLab
> GitLab Enterprise Edition contains [advanced Jenkins support][jenkins].
diff --git a/doc/integration/external-issue-tracker.md b/doc/integration/external-issue-tracker.md
index 2dd9b33273c..372e1909330 100644
--- a/doc/integration/external-issue-tracker.md
+++ b/doc/integration/external-issue-tracker.md
@@ -4,14 +4,12 @@ GitLab has a great issue tracker but you can also use an external one such as
Jira, Redmine, or Bugzilla. Issue trackers are configurable per GitLab project and allow
you to do the following:
-- the **Issues** link on the GitLab project pages takes you to the appropriate
- issue index of the external tracker
-- clicking **New issue** on the project dashboard creates a new issue on the
- external tracker
- you can reference these external issues inside GitLab interface
(merge requests, commits, comments) and they will be automatically converted
into links
+You can have enabled both external and internal GitLab issue trackers in parallel. The **Issues** link always opens the internal issue tracker and in case the internal issue tracker is disabled the link is not visible in the menu.
+
## Configuration
The configuration is done via a project's **Services**.
diff --git a/doc/user/index.md b/doc/user/index.md
new file mode 100644
index 00000000000..f545dbffde3
--- /dev/null
+++ b/doc/user/index.md
@@ -0,0 +1,175 @@
+# User documentation
+
+Welcome to GitLab! We're glad to have you here!
+
+As a GitLab user you'll have access to all the features
+your [subscription](https://about.gitlab.com/products/)
+includes, except [GitLab administrator](../README.md#administrator-documentation)
+settings, unless you have admin privileges to install, configure,
+and upgrade your GitLab instance.
+
+For GitLab.com, admin privileges are restricted to the GitLab team.
+
+If you run your own GitLab instance and are looking for the administration settings,
+please refer to the [administration](../README.md#administrator-documentation)
+documentation.
+
+## Overview
+
+GitLab is a fully integrated software development platform that enables you
+and your team to work cohesively, faster, transparently, and effectively,
+since the discussion of a new idea until taking that idea to production all
+all the way through, from within the same platform.
+
+Please check this page for an overview on [GitLab's features](https://about.gitlab.com/features/).
+
+## Use cases
+
+GitLab is a git-based platforms that integrates a great number of essential tools for software development and deployment, and project management:
+
+- Code hosting in repositories with version control
+- Track proposals for new implementations, bug reports, and feedback with a
+fully featured [Issue Tracker](project/issues/index.md#issue-tracker)
+- Organize and prioritize with [Issue Boards](project/issues/index.md#issue-boards)
+- Code review in [Merge Requests](project/merge_requests/index.md) with live-preview changes per
+branch with [Review Apps](../ci/review_apps/index.md)
+- Build, test and deploy with built-in [Continuous Integration](../ci/README.md)
+- Deploy your personal and professional static websites with [GitLab Pages](project/pages/index.md)
+- Integrate with Docker with [GitLab Container Registry](project/container_registry.md)
+- Track the development lifecycle with [GitLab Cycle Analytics](project/cycle_analytics.md)
+
+With GitLab Enterprise Edition, you can also:
+
+- Provide support with [Service Desk](https://docs.gitlab.com/ee/user/project/service_desk.html)
+- Improve collaboration with
+[Merge Request Approvals](https://docs.gitlab.com/ee/user/project/merge_requests/index.html#merge-request-approvals),
+[Multiple Assignees for Issues](https://docs.gitlab.com/ee/user/project/issues/multiple_assignees_for_issues.html),
+and [Multiple Issue Boards](https://docs.gitlab.com/ee/user/project/issue_board.html#multiple-issue-boards)
+- Create formal relashionships between issues with [Related Issues](https://docs.gitlab.com/ee/user/project/issues/related_issues.html)
+- Use [Burndown Charts](https://docs.gitlab.com/ee/user/project/milestones/burndown_charts.html) to track progress during a sprint or while working on a new version of their software.
+- Leverage [Elasticsearch](https://docs.gitlab.com/ee/integration/elasticsearch.html) with [Advanced Global Search](https://docs.gitlab.com/ee/user/search/advanced_global_search.html) and [Advanced Syntax Search](https://docs.gitlab.com/ee/user/search/advanced_search_syntax.html) for faster, more advanced code search across your entire GitLab instance
+- [Authenticate users with Kerberos](https://docs.gitlab.com/ee/integration/kerberos.html)
+- [Mirror a repository](https://docs.gitlab.com/ee/workflow/repository_mirroring.html) from elsewhere on your local server.
+- [Export issues as CSV](https://docs.gitlab.com/ee/user/project/issues/csv_export.html)
+- View your entire CI/CD pipeline involving more than one project with [Multiple-Project Pipeline Graphs](https://docs.gitlab.com/ee/ci/multi_project_pipeline_graphs.html)
+- [Lock files](https://docs.gitlab.com/ee/user/project/file_lock.html) to prevent conflicts
+- View of the current health and status of each CI environment running on Kubernetes with [Deploy Boards](https://docs.gitlab.com/ee/user/project/deploy_boards.html)
+- Leverage your continuous delivery method with [Canary Deployments](https://docs.gitlab.com/ee/user/project/canary_deployments.html)
+
+You can also [integrate](project/integrations/project_services.md) GitLab with numerous third-party applications, such as Mattermost, Microsoft Teams, HipChat, Trello, Slack, Bamboo CI, JIRA, and a lot more.
+
+### Articles
+
+For a complete workflow use case please check [GitLab Workflow, an Overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/#gitlab-workflow-use-case-scenario).
+
+For more use cases please check our [Technical Articles](../articles/index.md).
+
+## Projects
+
+In GitLab, you can create projects for numerous reasons, such as, host
+your code, use it as an issue tracker, collaborate on code, and continuously
+build, test, and deploy your app with built-in GitLab CI/CD. Or, you can do
+it all at once, from one single project.
+
+### Issues
+
+Explore the best of GitLab [Issues](project/issues/index.md).
+
+### Merge Requests
+
+Collanorate on code, gather reviews, live preview changes per branch, and
+request approvals with [Merge Requests](project/merge_requests/index.md).
+
+### Milestones
+
+Work on multiple issues and merge requests towards the same target date
+with [Milestones](project/milestones/index.md).
+
+### GitLab Pages
+
+Publish your static site directly from GitLab with [GitLab Pages](project/pages/index.md). You
+can [build, test, and deploy any Static Site Generator](https://about.gitlab.com/2016/06/17/ssg-overview-gitlab-pages-part-3-examples-ci/) with Pages.
+
+### Container Registry
+
+Build and deploy Docker images with [GitLab Container Registry](project/container_registry.md).
+
+## GitLab CI/CD
+
+Use built-in [GitLab CI/CD](../ci/README.md) to test, build, and deploy your applications
+directly from GitLab. No third-party integrations needed.
+
+### Auto Deploy
+
+Deploy your application out-of-the-box with [GitLab Auto Deploy](../ci/autodeploy/index.md).
+
+### Review Apps
+
+Live-preview the changes introduced by a merge request with [Review Apps](../ci/review_apps/index.md).
+
+## Groups
+
+With GitLab [Groups](group/index.md) you can assemble related projects together
+and grant members access to several projects at once.
+
+### Subgroups
+
+Groups can also be nested in [subgroups](group/subgroups/index.md).
+
+## Account
+
+There is a lot you can customize and configure
+to enjoy the best of GitLab.
+
+Manage your user settings to change your personal info,
+personal access tokens, authorized applications, integrations, etc.
+
+### Authentication
+
+Read through the [authentication](../topics/authentication/index.md) methods available in GitLab.
+
+### Permissions
+
+Learn the different set of [permissions](permissions.md) for user type (guest, reporter, developer, master, owner).
+
+## Integrations
+
+[Integrate GitLab](../integration/README.md) with your preferred tool,
+such as Trello, JIRA, etc.
+
+## Git and GitLab
+
+Learn what is [Git](../topics/git/index.md) and its best practices.
+
+## Discussions
+
+In GitLab, you can comment and mention collaborators in issues,
+merge requests, code snippets, and commits.
+
+When performing inline reviews to implementations
+to your codebase through merge requests you can
+gather feedback through [resolvable discussions](discussions/index.md#resolvable-discussions).
+
+## Todos
+
+Never forget to reply to your collaborators. [GitLab Todos](../workflow/todos.md)
+are a tool for working faster and more effectively with your team,
+by listing all user or group mentions, as well as issues and merge
+requests you're assigned to.
+
+## Snippets
+
+[Snippets](snippets.md) are code blocks that you want to store in GitLab, from which
+you have quick access to. You can also gather feedback on them through
+[discussions](#discussions).
+
+## Webhooks
+
+Configure [webhooks](project/integrations/webhooks.html) to listen for
+specific events like pushes, issues or merge requests. GitLab will send a
+POST request with data to the webhook URL.
+
+## API
+
+Automate GitLab via [API](../api/README.html).
+
diff --git a/doc/user/project/integrations/bugzilla.md b/doc/user/project/integrations/bugzilla.md
index 6a040516231..ba2adc1afda 100644
--- a/doc/user/project/integrations/bugzilla.md
+++ b/doc/user/project/integrations/bugzilla.md
@@ -20,10 +20,12 @@ Once you have configured and enabled Bugzilla:
## Referencing issues in Bugzilla
Issues in Bugzilla can be referenced in two alternative ways:
-1. `#<ID>` where `<ID>` is a number (example `#143`)
+1. `#<ID>` where `<ID>` is a number (example `#143`).
2. `<PROJECT>-<ID>` where `<PROJECT>` starts with a capital letter which is
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
+We suggest using the longer format if you have both internal and external issue trackers enabled. If you use the shorter format and an issue with the same ID exists in the internal issue tracker the internal issue will be linked.
+
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.
diff --git a/doc/user/project/integrations/jira.md b/doc/user/project/integrations/jira.md
index cf03f2a9033..cfa4c8a93f8 100644
--- a/doc/user/project/integrations/jira.md
+++ b/doc/user/project/integrations/jira.md
@@ -98,11 +98,11 @@ in the table below.
| Field | Description |
| ----- | ----------- |
| `Web URL` | The base URL to the JIRA instance web interface which is being linked to this GitLab project. E.g., `https://jira.example.com`. |
-| `JIRA API URL` | The base URL to the JIRA instance API. E.g., `https://jira-api.example.com`. This is optional. If not entered, the Web URL value be used. |
+| `JIRA API URL` | The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., `https://jira-api.example.com`. |
| `Project key` | Put a JIRA project key (in uppercase), e.g. `MARS` in this field. This is only for testing the configuration settings. JIRA integration in GitLab works with _all_ JIRA projects in your JIRA instance. This field will be removed in a future release. |
| `Username` | The user name created in [configuring JIRA step](#configuring-jira). |
| `Password` |The password of the user created in [configuring JIRA step](#configuring-jira). |
-| `JIRA issue transition` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
+| `Transition ID` | This is the ID of a transition that moves issues to a closed state. You can find this number under JIRA workflow administration ([see screenshot](img/jira_workflow_screenshot.png)). **Closing JIRA issues via commits or Merge Requests won't work if you don't set the ID correctly.** |
After saving the configuration, your GitLab project will be able to interact
with all JIRA projects in your JIRA instance.
diff --git a/doc/user/project/integrations/redmine.md b/doc/user/project/integrations/redmine.md
index 8026f1f57bc..cf92465da53 100644
--- a/doc/user/project/integrations/redmine.md
+++ b/doc/user/project/integrations/redmine.md
@@ -30,5 +30,7 @@ Issues in Redmine can be referenced in two alternative ways:
then followed by capital letters, numbers or underscores, and `<ID>` is
a number (example `API_32-143`).
+We suggest using the longer format if you have both internal and external issue trackers enabled. If you use the shorter format and an issue with the same ID exists in the internal issue tracker the internal issue will be linked.
+
Please note that `<PROJECT>` part is ignored and links always point to the
address specified in `issues_url`.
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 258b3a2f955..9ad15a12c3c 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -71,9 +71,10 @@ The next time a pipeline is scheduled, your credentials will be used.
>**Note:**
When the owner of the schedule doesn't have the ability to create pipelines
-anymore, due to e.g., being blocked or removed from the project, the schedule
-is deactivated. Another user can take ownership and activate it, so the
-schedule can be run again.
+anymore, due to e.g., being blocked or removed from the project, or lacking
+the permission to run on protected branches or tags. When this happened, the
+schedule is deactivated. Another user can take ownership and activate it, so
+the schedule can be run again.
## Advanced admin configuration
diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md
index 19b51c83222..ce4dd4e99d5 100644
--- a/doc/user/project/quick_actions.md
+++ b/doc/user/project/quick_actions.md
@@ -37,3 +37,4 @@ do.
| `/target_branch <Branch Name>` | Set target branch for current merge request |
| `/award :emoji:` | Toggle award for :emoji: |
| `/board_move ~column` | Move issue to column on the board |
+| `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue |
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 906a81b29b3..7e2a357f6b2 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -175,7 +175,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
fill_in 'JIRA API URL', with: 'http://jira.example/api'
fill_in 'Username', with: 'gitlab'
fill_in 'Password', with: 'gitlab'
- fill_in 'Project Key', with: 'GITLAB'
click_button 'Save'
end
diff --git a/features/steps/project/wiki.rb b/features/steps/project/wiki.rb
index a2f5d2e1515..9d38939378d 100644
--- a/features/steps/project/wiki.rb
+++ b/features/steps/project/wiki.rb
@@ -114,7 +114,7 @@ class Spinach::Features::ProjectWiki < Spinach::FeatureSteps
end
step 'Image should be shown on the page' do
- expect(page).to have_xpath("//img[@src=\"image.jpg\"]")
+ expect(page).to have_xpath("//img[@data-src=\"image.jpg\"]")
end
step 'I click on image link' do
diff --git a/lib/api/api.rb b/lib/api/api.rb
index efcf0976a81..3bdafa3edc1 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -3,6 +3,7 @@ module API
include APIGuard
allow_access_with_scope :api
+ prefix :api
version %w(v3 v4), using: :path
@@ -109,7 +110,8 @@ module API
mount ::API::Members
mount ::API::MergeRequestDiffs
mount ::API::MergeRequests
- mount ::API::Milestones
+ mount ::API::ProjectMilestones
+ mount ::API::GroupMilestones
mount ::API::Namespaces
mount ::API::Notes
mount ::API::NotificationSettings
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 09a88869063..5cdc441e8cb 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -82,6 +82,38 @@ module API
end
class Project < Grape::Entity
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose :_links do
+ expose :self do |project|
+ expose_url(api_v4_projects_path(id: project.id))
+ end
+
+ expose :issues, if: -> (*args) { issues_available?(*args) } do |project|
+ expose_url(api_v4_projects_issues_path(id: project.id))
+ end
+
+ expose :merge_requests, if: -> (*args) { mrs_available?(*args) } do |project|
+ expose_url(api_v4_projects_merge_requests_path(id: project.id))
+ end
+
+ expose :repo_branches do |project|
+ expose_url(api_v4_projects_repository_branches_path(id: project.id))
+ end
+
+ expose :labels do |project|
+ expose_url(api_v4_projects_labels_path(id: project.id))
+ end
+
+ expose :events do |project|
+ expose_url(api_v4_projects_events_path(id: project.id))
+ end
+
+ expose :members do |project|
+ expose_url(api_v4_projects_members_path(id: project.id))
+ end
+ end
+
expose :id, :description, :default_branch, :tag_list
expose :archived?, as: :archived
expose :visibility, :ssh_url_to_repo, :http_url_to_repo, :web_url
@@ -109,7 +141,7 @@ module API
user.avatar_url(only_path: false)
end
expose :star_count, :forks_count
- expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) && project.default_issues_tracker? }
+ expose :open_issues_count, if: lambda { |project, options| project.feature_available?(:issues, options[:current_user]) }
expose :runners_token, if: lambda { |_project, options| options[:user_can_admin_project] }
expose :public_builds, as: :public_jobs
expose :ci_config_path
@@ -269,8 +301,8 @@ module API
class Milestone < Grape::Entity
expose :id, :iid
- expose(:project_id) { |entity| entity&.project_id }
- expose(:group_id) { |entity| entity&.group_id }
+ expose :project_id, if: -> (entity, options) { entity&.project_id }
+ expose :group_id, if: -> (entity, options) { entity&.group_id }
expose :title, :description
expose :state, :created_at, :updated_at
expose :due_date
@@ -297,6 +329,26 @@ module API
end
class Issue < IssueBasic
+ include ::API::Helpers::RelatedResourcesHelpers
+
+ expose :_links do
+ expose :self do |issue|
+ expose_url(api_v4_project_issue_path(id: issue.project_id, issue_iid: issue.iid))
+ end
+
+ expose :notes do |issue|
+ expose_url(api_v4_projects_issues_notes_path(id: issue.project_id, noteable_id: issue.iid))
+ end
+
+ expose :award_emoji do |issue|
+ expose_url(api_v4_projects_issues_award_emoji_path(id: issue.project_id, issue_iid: issue.iid))
+ end
+
+ expose :project do |issue|
+ expose_url(api_v4_projects_path(id: issue.project_id))
+ end
+ end
+
expose :subscribed do |issue, options|
issue.subscribed?(options[:current_user], options[:project] || issue.project)
end
diff --git a/lib/api/group_milestones.rb b/lib/api/group_milestones.rb
new file mode 100644
index 00000000000..b85eb59dc0a
--- /dev/null
+++ b/lib/api/group_milestones.rb
@@ -0,0 +1,85 @@
+module API
+ class GroupMilestones < Grape::API
+ include MilestoneResponses
+ include PaginationParams
+
+ before do
+ authenticate!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a group'
+ end
+ resource :groups, requirements: { id: %r{[^/]+} } do
+ desc 'Get a list of group milestones' do
+ success Entities::Milestone
+ end
+ params do
+ use :list_params
+ end
+ get ":id/milestones" do
+ list_milestones_for(user_group)
+ end
+
+ desc 'Get a single group milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a group milestone'
+ end
+ get ":id/milestones/:milestone_id" do
+ authorize! :read_group, user_group
+
+ get_milestone_for(user_group)
+ end
+
+ desc 'Create a new group milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the milestone'
+ use :optional_params
+ end
+ post ":id/milestones" do
+ authorize! :admin_milestones, user_group
+
+ create_milestone_for(user_group)
+ end
+
+ desc 'Update an existing group milestone' do
+ success Entities::Milestone
+ end
+ params do
+ use :update_params
+ end
+ put ":id/milestones/:milestone_id" do
+ authorize! :admin_milestones, user_group
+
+ update_milestone_for(user_group)
+ end
+
+ desc 'Get all issues for a single group milestone' do
+ success Entities::IssueBasic
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a group milestone'
+ use :pagination
+ end
+ get ":id/milestones/:milestone_id/issues" do
+ milestone_issuables_for(user_group, :issue)
+ end
+
+ desc 'Get all merge requests for a single group milestone' do
+ detail 'This feature was introduced in GitLab 9.'
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a group milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/merge_requests' do
+ milestone_issuables_for(user_group, :merge_request)
+ end
+ end
+ end
+end
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 0f4791841d2..57e3e93500f 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -25,6 +25,10 @@ module API
initial_current_user != current_user
end
+ def user_group
+ @group ||= find_group!(params[:id])
+ end
+
def user_project
@project ||= find_project!(params[:id])
end
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
new file mode 100644
index 00000000000..769cc1457fc
--- /dev/null
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -0,0 +1,28 @@
+module API
+ module Helpers
+ module RelatedResourcesHelpers
+ include GrapeRouteHelpers::NamedRouteMatcher
+
+ def issues_available?(project, options)
+ available?(:issues, project, options[:current_user])
+ end
+
+ def mrs_available?(project, options)
+ available?(:merge_requests, project, options[:current_user])
+ end
+
+ def expose_url(path)
+ url_options = Rails.application.routes.default_url_options
+ protocol, host, port = url_options.slice(:protocol, :host, :port).values
+
+ URI::HTTP.build(scheme: protocol, host: host, port: port, path: path).to_s
+ end
+
+ private
+
+ def available?(feature, project, current_user)
+ project.feature_available?(feature, current_user)
+ end
+ end
+ end
+end
diff --git a/lib/api/issues.rb b/lib/api/issues.rb
index 14b26f28ebf..93ebe18508d 100644
--- a/lib/api/issues.rb
+++ b/lib/api/issues.rb
@@ -112,7 +112,7 @@ module API
params do
requires :issue_iid, type: Integer, desc: 'The internal ID of a project issue'
end
- get ":id/issues/:issue_iid" do
+ get ":id/issues/:issue_iid", as: :api_v4_project_issue do
issue = find_project_issue(params[:issue_iid])
present issue, with: Entities::Issue, current_user: current_user, project: user_project
end
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 6e2e13e0a24..f64ac659413 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -29,14 +29,6 @@ module API
render_api_error!(errors, 400)
end
- def issue_entity(project)
- if project.has_external_issue_tracker?
- Entities::ExternalIssue
- else
- Entities::IssueBasic
- end
- end
-
def find_merge_requests(args = {})
args = params.merge(args)
@@ -278,7 +270,14 @@ module API
get ':id/merge_requests/:merge_request_iid/closes_issues' do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
issues = ::Kaminari.paginate_array(merge_request.closes_issues(current_user))
- present paginate(issues), with: issue_entity(user_project), current_user: current_user
+ issues = paginate(issues)
+
+ external_issues, internal_issues = issues.partition { |issue| issue.is_a?(ExternalIssue) }
+
+ data = Entities::IssueBasic.represent(internal_issues, current_user: current_user)
+ data += Entities::ExternalIssue.represent(external_issues, current_user: current_user)
+
+ data.as_json
end
end
end
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
new file mode 100644
index 00000000000..ef09d9505d2
--- /dev/null
+++ b/lib/api/milestone_responses.rb
@@ -0,0 +1,98 @@
+module API
+ module MilestoneResponses
+ extend ActiveSupport::Concern
+
+ included do
+ helpers do
+ params :optional_params do
+ optional :description, type: String, desc: 'The description of the milestone'
+ optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
+ optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
+ end
+
+ params :list_params do
+ optional :state, type: String, values: %w[active closed all], default: 'all',
+ desc: 'Return "active", "closed", or "all" milestones'
+ optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
+ optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
+ use :pagination
+ end
+
+ params :update_params do
+ requires :milestone_id, type: Integer, desc: 'The milestone ID number'
+ optional :title, type: String, desc: 'The title of the milestone'
+ optional :state_event, type: String, values: %w[close activate],
+ desc: 'The state event of the milestone '
+ use :optional_params
+ at_least_one_of :title, :description, :due_date, :state_event
+ end
+
+ def list_milestones_for(parent)
+ milestones = parent.milestones
+ milestones = Milestone.filter_by_state(milestones, params[:state])
+ milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
+ milestones = filter_by_search(milestones, params[:search]) if params[:search]
+
+ present paginate(milestones), with: Entities::Milestone
+ end
+
+ def get_milestone_for(parent)
+ milestone = parent.milestones.find(params[:milestone_id])
+ present milestone, with: Entities::Milestone
+ end
+
+ def create_milestone_for(parent)
+ milestone = ::Milestones::CreateService.new(parent, current_user, declared_params).execute
+
+ if milestone.valid?
+ present milestone, with: Entities::Milestone
+ else
+ render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400)
+ end
+ end
+
+ def update_milestone_for(parent)
+ milestone = parent.milestones.find(params.delete(:milestone_id))
+
+ milestone_params = declared_params(include_missing: false)
+ milestone = ::Milestones::UpdateService.new(parent, current_user, milestone_params).execute(milestone)
+
+ if milestone.valid?
+ present milestone, with: Entities::Milestone
+ else
+ render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400)
+ end
+ end
+
+ def milestone_issuables_for(parent, type)
+ milestone = parent.milestones.find(params[:milestone_id])
+
+ finder_klass, entity = get_finder_and_entity(type)
+
+ params = build_finder_params(milestone, parent)
+
+ issuables = finder_klass.new(current_user, params).execute
+ present paginate(issuables), with: entity, current_user: current_user
+ end
+
+ def build_finder_params(milestone, parent)
+ finder_params = { milestone_title: milestone.title, sort: 'label_priority' }
+
+ if parent.is_a?(Group)
+ finder_params.merge(group_id: parent.id)
+ else
+ finder_params.merge(project_id: parent.id)
+ end
+ end
+
+ def get_finder_and_entity(type)
+ if type == :issue
+ [IssuesFinder, Entities::IssueBasic]
+ else
+ [MergeRequestsFinder, Entities::MergeRequestBasic]
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/api/milestones.rb b/lib/api/milestones.rb
deleted file mode 100644
index 3541d3c95fb..00000000000
--- a/lib/api/milestones.rb
+++ /dev/null
@@ -1,154 +0,0 @@
-module API
- class Milestones < Grape::API
- include PaginationParams
-
- before { authenticate! }
-
- helpers do
- def filter_milestones_state(milestones, state)
- case state
- when 'active' then milestones.active
- when 'closed' then milestones.closed
- else milestones
- end
- end
-
- params :optional_params do
- optional :description, type: String, desc: 'The description of the milestone'
- optional :due_date, type: String, desc: 'The due date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
- optional :start_date, type: String, desc: 'The start date of the milestone. The ISO 8601 date format (%Y-%m-%d)'
- end
- end
-
- params do
- requires :id, type: String, desc: 'The ID of a project'
- end
- resource :projects, requirements: { id: %r{[^/]+} } do
- desc 'Get a list of project milestones' do
- success Entities::Milestone
- end
- params do
- optional :state, type: String, values: %w[active closed all], default: 'all',
- desc: 'Return "active", "closed", or "all" milestones'
- optional :iids, type: Array[Integer], desc: 'The IIDs of the milestones'
- optional :search, type: String, desc: 'The search criteria for the title or description of the milestone'
- use :pagination
- end
- get ":id/milestones" do
- authorize! :read_milestone, user_project
-
- milestones = user_project.milestones
- milestones = filter_milestones_state(milestones, params[:state])
- milestones = filter_by_iid(milestones, params[:iids]) if params[:iids].present?
- milestones = filter_by_search(milestones, params[:search]) if params[:search]
-
- present paginate(milestones), with: Entities::Milestone
- end
-
- desc 'Get a single project milestone' do
- success Entities::Milestone
- end
- params do
- requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
- end
- get ":id/milestones/:milestone_id" do
- authorize! :read_milestone, user_project
-
- milestone = user_project.milestones.find(params[:milestone_id])
- present milestone, with: Entities::Milestone
- end
-
- desc 'Create a new project milestone' do
- success Entities::Milestone
- end
- params do
- requires :title, type: String, desc: 'The title of the milestone'
- use :optional_params
- end
- post ":id/milestones" do
- authorize! :admin_milestone, user_project
-
- milestone = ::Milestones::CreateService.new(user_project, current_user, declared_params).execute
-
- if milestone.valid?
- present milestone, with: Entities::Milestone
- else
- render_api_error!("Failed to create milestone #{milestone.errors.messages}", 400)
- end
- end
-
- desc 'Update an existing project milestone' do
- success Entities::Milestone
- end
- params do
- requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
- optional :title, type: String, desc: 'The title of the milestone'
- optional :state_event, type: String, values: %w[close activate],
- desc: 'The state event of the milestone '
- use :optional_params
- at_least_one_of :title, :description, :due_date, :state_event
- end
- put ":id/milestones/:milestone_id" do
- authorize! :admin_milestone, user_project
- milestone = user_project.milestones.find(params.delete(:milestone_id))
-
- milestone_params = declared_params(include_missing: false)
- milestone = ::Milestones::UpdateService.new(user_project, current_user, milestone_params).execute(milestone)
-
- if milestone.valid?
- present milestone, with: Entities::Milestone
- else
- render_api_error!("Failed to update milestone #{milestone.errors.messages}", 400)
- end
- end
-
- desc 'Get all issues for a single project milestone' do
- success Entities::IssueBasic
- end
- params do
- requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
- use :pagination
- end
- get ":id/milestones/:milestone_id/issues" do
- authorize! :read_milestone, user_project
-
- milestone = user_project.milestones.find(params[:milestone_id])
-
- finder_params = {
- project_id: user_project.id,
- milestone_title: milestone.title,
- sort: 'label_priority'
- }
-
- issues = IssuesFinder.new(current_user, finder_params).execute
- present paginate(issues), with: Entities::IssueBasic, current_user: current_user, project: user_project
- end
-
- desc 'Get all merge requests for a single project milestone' do
- detail 'This feature was introduced in GitLab 9.'
- success Entities::MergeRequestBasic
- end
- params do
- requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
- use :pagination
- end
- get ':id/milestones/:milestone_id/merge_requests' do
- authorize! :read_milestone, user_project
-
- milestone = user_project.milestones.find(params[:milestone_id])
-
- finder_params = {
- project_id: user_project.id,
- milestone_title: milestone.title,
- sort: 'label_priority'
- }
-
- merge_requests = MergeRequestsFinder.new(current_user, finder_params).execute
- present paginate(merge_requests),
- with: Entities::MergeRequestBasic,
- current_user: current_user,
- project: user_project
- end
- end
- end
-end
diff --git a/lib/api/project_milestones.rb b/lib/api/project_milestones.rb
new file mode 100644
index 00000000000..451998c726a
--- /dev/null
+++ b/lib/api/project_milestones.rb
@@ -0,0 +1,91 @@
+module API
+ class ProjectMilestones < Grape::API
+ include PaginationParams
+ include MilestoneResponses
+
+ before do
+ authenticate!
+ end
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+ resource :projects, requirements: { id: %r{[^/]+} } do
+ desc 'Get a list of project milestones' do
+ success Entities::Milestone
+ end
+ params do
+ use :list_params
+ end
+ get ":id/milestones" do
+ authorize! :read_milestone, user_project
+
+ list_milestones_for(user_project)
+ end
+
+ desc 'Get a single project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ end
+ get ":id/milestones/:milestone_id" do
+ authorize! :read_milestone, user_project
+
+ get_milestone_for(user_project)
+ end
+
+ desc 'Create a new project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ requires :title, type: String, desc: 'The title of the milestone'
+ use :optional_params
+ end
+ post ":id/milestones" do
+ authorize! :admin_milestone, user_project
+
+ create_milestone_for(user_project)
+ end
+
+ desc 'Update an existing project milestone' do
+ success Entities::Milestone
+ end
+ params do
+ use :update_params
+ end
+ put ":id/milestones/:milestone_id" do
+ authorize! :admin_milestone, user_project
+
+ update_milestone_for(user_project)
+ end
+
+ desc 'Get all issues for a single project milestone' do
+ success Entities::IssueBasic
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ":id/milestones/:milestone_id/issues" do
+ authorize! :read_milestone, user_project
+
+ milestone_issuables_for(user_project, :issue)
+ end
+
+ desc 'Get all merge requests for a single project milestone' do
+ detail 'This feature was introduced in GitLab 9.'
+ success Entities::MergeRequestBasic
+ end
+ params do
+ requires :milestone_id, type: Integer, desc: 'The ID of a project milestone'
+ use :pagination
+ end
+ get ':id/milestones/:milestone_id/merge_requests' do
+ authorize! :read_milestone, user_project
+
+ milestone_issuables_for(user_project, :merge_request)
+ end
+ end
+ end
+end
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 7488f95a9b7..843c05ae32e 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -313,12 +313,6 @@ module API
desc: 'The base URL to the JIRA instance API. Web URL value will be used if not set. E.g., https://jira-api.example.com'
},
{
- required: true,
- name: :project_key,
- type: String,
- desc: 'The short identifier for your JIRA project, all uppercase, e.g., PROJ'
- },
- {
required: false,
name: :username,
type: String,
diff --git a/lib/api/triggers.rb b/lib/api/triggers.rb
index cb0619105e1..9375e7eb768 100644
--- a/lib/api/triggers.rb
+++ b/lib/api/triggers.rb
@@ -27,12 +27,13 @@ module API
end
# create request and trigger builds
- trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
- if trigger_request
- present trigger_request.pipeline, with: Entities::Pipeline
+ result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables)
+ pipeline = result.pipeline
+
+ if pipeline.persisted?
+ present pipeline, with: Entities::Pipeline
else
- errors = 'No pipeline created'
- render_api_error!(errors, 400)
+ render_validation_error!(pipeline)
end
end
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 3759250f7f6..773f667abe0 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -259,11 +259,40 @@ module API
expose :job_events, as: :build_events
end
- class Issue < ::API::Entities::Issue
+ class ProjectEntity < Grape::Entity
+ expose :id, :iid
+ expose(:project_id) { |entity| entity&.project.try(:id) }
+ expose :title, :description
+ expose :state, :created_at, :updated_at
+ end
+
+ class IssueBasic < ProjectEntity
+ expose :label_names, as: :labels
+ expose :milestone, using: ::API::Entities::Milestone
+ expose :assignees, :author, using: ::API::Entities::UserBasic
+
+ expose :assignee, using: ::API::Entities::UserBasic do |issue, options|
+ issue.assignees.first
+ end
+
+ expose :user_notes_count
+ expose :upvotes, :downvotes
+ expose :due_date
+ expose :confidential
+
+ expose :web_url do |issue, options|
+ Gitlab::UrlBuilder.build(issue)
+ end
+ end
+
+ class Issue < IssueBasic
unexpose :assignees
expose :assignee do |issue, options|
::API::Entities::UserBasic.represent(issue.assignees.first, options)
end
+ expose :subscribed do |issue, options|
+ issue.subscribed?(options[:current_user], options[:project] || issue.project)
+ end
end
end
end
diff --git a/lib/api/v3/triggers.rb b/lib/api/v3/triggers.rb
index a23d6b6b48c..e9d4c35307b 100644
--- a/lib/api/v3/triggers.rb
+++ b/lib/api/v3/triggers.rb
@@ -28,12 +28,13 @@ module API
end
# create request and trigger builds
- trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref].to_s, variables)
- if trigger_request
- present trigger_request, with: ::API::V3::Entities::TriggerRequest
+ result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref].to_s, variables)
+ pipeline = result.pipeline
+
+ if pipeline.persisted?
+ present result.trigger_request, with: ::API::V3::Entities::TriggerRequest
else
- errors = 'No builds created'
- render_api_error!(errors, 400)
+ render_validation_error!(pipeline)
end
end
diff --git a/lib/banzai/filter/gollum_tags_filter.rb b/lib/banzai/filter/gollum_tags_filter.rb
index 0ea4eeaed5b..2e259904673 100644
--- a/lib/banzai/filter/gollum_tags_filter.rb
+++ b/lib/banzai/filter/gollum_tags_filter.rb
@@ -118,7 +118,7 @@ module Banzai
end
if path
- content_tag(:img, nil, src: path, class: 'gfm')
+ content_tag(:img, nil, data: { src: path }, class: 'gfm')
end
end
diff --git a/lib/banzai/filter/image_lazy_load_filter.rb b/lib/banzai/filter/image_lazy_load_filter.rb
new file mode 100644
index 00000000000..7a81d583b82
--- /dev/null
+++ b/lib/banzai/filter/image_lazy_load_filter.rb
@@ -0,0 +1,16 @@
+module Banzai
+ module Filter
+ # HTML filter that moves the value of the src attribute to the data-src attribute so it can be lazy loaded
+ class ImageLazyLoadFilter < HTML::Pipeline::Filter
+ def call
+ doc.xpath('descendant-or-self::img').each do |img|
+ img['class'] ||= '' << 'lazy'
+ img['data-src'] = img['src']
+ img['src'] = LazyImageTagHelper.placeholder_image
+ end
+
+ doc
+ end
+ end
+ end
+end
diff --git a/lib/banzai/filter/image_link_filter.rb b/lib/banzai/filter/image_link_filter.rb
index 123c92fd250..f318c425962 100644
--- a/lib/banzai/filter/image_link_filter.rb
+++ b/lib/banzai/filter/image_link_filter.rb
@@ -10,7 +10,7 @@ module Banzai
link = doc.document.create_element(
'a',
class: 'no-attachment-icon',
- href: img['src'],
+ href: img['data-src'] || img['src'],
target: '_blank',
rel: 'noopener noreferrer'
)
diff --git a/lib/banzai/filter/issue_reference_filter.rb b/lib/banzai/filter/issue_reference_filter.rb
index ba1a5ac84b3..ce1ab977d3b 100644
--- a/lib/banzai/filter/issue_reference_filter.rb
+++ b/lib/banzai/filter/issue_reference_filter.rb
@@ -20,7 +20,7 @@ module Banzai
end
def url_for_object(issue, project)
- IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path])
+ IssuesHelper.url_for_issue(issue.iid, project, only_path: context[:only_path], internal: true)
end
def project_from_ref(ref)
diff --git a/lib/banzai/filter/relative_link_filter.rb b/lib/banzai/filter/relative_link_filter.rb
index 9e23c8f8c55..c2fed57a0d8 100644
--- a/lib/banzai/filter/relative_link_filter.rb
+++ b/lib/banzai/filter/relative_link_filter.rb
@@ -22,6 +22,7 @@ module Banzai
doc.css('img, video').each do |el|
process_link_attr el.attribute('src')
+ process_link_attr el.attribute('data-src')
end
doc
diff --git a/lib/banzai/pipeline/gfm_pipeline.rb b/lib/banzai/pipeline/gfm_pipeline.rb
index bd4d1aa9ff8..3208abfc538 100644
--- a/lib/banzai/pipeline/gfm_pipeline.rb
+++ b/lib/banzai/pipeline/gfm_pipeline.rb
@@ -16,6 +16,7 @@ module Banzai
Filter::MathFilter,
Filter::UploadLinkFilter,
Filter::VideoLinkFilter,
+ Filter::ImageLazyLoadFilter,
Filter::ImageLinkFilter,
Filter::EmojiFilter,
Filter::TableOfContentsFilter,
diff --git a/lib/banzai/reference_parser/external_issue_parser.rb b/lib/banzai/reference_parser/external_issue_parser.rb
index 6307c1b571a..1802cd04854 100644
--- a/lib/banzai/reference_parser/external_issue_parser.rb
+++ b/lib/banzai/reference_parser/external_issue_parser.rb
@@ -21,10 +21,14 @@ module Banzai
gather_attributes_per_project(nodes, self.class.data_attribute)
end
- private
-
+ # we extract only external issue trackers references here, we don't extract cross-project references,
+ # so we don't need to do anything here.
def can_read_reference?(user, ref_project, node)
- can?(user, :read_issue, ref_project)
+ true
+ end
+
+ def nodes_visible_to_user(user, nodes)
+ nodes
end
end
end
diff --git a/lib/ci/api/triggers.rb b/lib/ci/api/triggers.rb
index 6e622601680..6225203f223 100644
--- a/lib/ci/api/triggers.rb
+++ b/lib/ci/api/triggers.rb
@@ -24,12 +24,13 @@ module Ci
end
# create request and trigger builds
- trigger_request = Ci::CreateTriggerRequestService.new.execute(project, trigger, params[:ref], variables)
- if trigger_request
- present trigger_request, with: Entities::TriggerRequest
+ result = Ci::CreateTriggerRequestService.execute(project, trigger, params[:ref], variables)
+ pipeline = result.pipeline
+
+ if pipeline.persisted?
+ present result.trigger_request, with: Entities::TriggerRequest
else
- errors = 'No builds created'
- render_api_error!(errors, 400)
+ render_validation_error!(pipeline)
end
end
end
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 63eebadff2e..3e27fd7b682 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -45,6 +45,8 @@ module Gitlab
:bare?,
to: :rugged
+ delegate :exists?, to: :gitaly_repository_client
+
# Default branch in the repository
def root_ref
@root_ref ||= gitaly_migrate(:root_ref) do |is_enabled|
@@ -208,10 +210,6 @@ module Gitlab
!empty?
end
- def repo_exists?
- !!rugged
- end
-
# Discovers the default branch based on the repository's available branches
#
# - If no branches are present, returns nil
@@ -815,6 +813,10 @@ module Gitlab
@gitaly_commit_client ||= Gitlab::GitalyClient::CommitService.new(self)
end
+ def gitaly_repository_client
+ @gitaly_repository_client ||= Gitlab::GitalyClient::RepositoryService.new(self)
+ end
+
private
# Gitaly note: JV: Trying to get rid of the 'filter' option so we can implement this with 'git'.
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 435e41e36fb..c90ef282fdd 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -57,7 +57,7 @@ module Gitlab
metadata = yield(metadata) if block_given?
stub(service, storage).send(rpc, request, metadata)
end
-
+
def self.request_metadata(storage)
encoded_token = Base64.strict_encode64(token(storage).to_s)
{ metadata: { 'authorization' => "Bearer #{encoded_token}" } }
diff --git a/lib/gitlab/gitaly_client/repository_service.rb b/lib/gitlab/gitaly_client/repository_service.rb
new file mode 100644
index 00000000000..f5d84ea8762
--- /dev/null
+++ b/lib/gitlab/gitaly_client/repository_service.rb
@@ -0,0 +1,16 @@
+module Gitlab
+ module GitalyClient
+ class RepositoryService
+ def initialize(repository)
+ @repository = repository
+ @gitaly_repo = repository.gitaly_repository
+ end
+
+ def exists?
+ request = Gitaly::RepositoryExistsRequest.new(repository: @gitaly_repo)
+
+ GitalyClient.call(@repository.storage, :repository_service, :exists, request).exists
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/path_regex.rb b/lib/gitlab/path_regex.rb
index 60a32d5d5ea..894bd5efae5 100644
--- a/lib/gitlab/path_regex.rb
+++ b/lib/gitlab/path_regex.rb
@@ -14,42 +14,42 @@ module Gitlab
TOP_LEVEL_ROUTES = %w[
-
.well-known
+ 404.html
+ 422.html
+ 500.html
+ 502.html
+ 503.html
abuse_reports
admin
- all
api
+ apple-touch-icon-precomposed.png
+ apple-touch-icon.png
assets
autocomplete
ci
dashboard
+ deploy.html
explore
+ favicon.ico
files
groups
health_check
help
- hooks
import
invites
- issues
jwt
koding
- member
- merge_requests
- new
- notes
notification_settings
oauth
profile
projects
public
- repository
robots.txt
s
search
sent_notifications
- services
+ slash-command-logo.png
snippets
- teams
u
unicorn_test
unsubscribes
diff --git a/lib/gitlab/reference_extractor.rb b/lib/gitlab/reference_extractor.rb
index 7668ecacc4b..f5b757ace77 100644
--- a/lib/gitlab/reference_extractor.rb
+++ b/lib/gitlab/reference_extractor.rb
@@ -33,7 +33,12 @@ module Gitlab
def issues
if project && project.jira_tracker?
- @references[:external_issue] ||= references(:external_issue)
+ if project.issues_enabled?
+ @references[:all_issues] ||= references(:external_issue) + references(:issue)
+ else
+ @references[:external_issue] ||= references(:external_issue) +
+ references(:issue).select { |i| i.project_id != project.id }
+ end
else
@references[:issue] ||= references(:issue)
end
diff --git a/lib/gitlab/slash_commands/issue_command.rb b/lib/gitlab/slash_commands/issue_command.rb
index 87ea19b8806..3d96982b820 100644
--- a/lib/gitlab/slash_commands/issue_command.rb
+++ b/lib/gitlab/slash_commands/issue_command.rb
@@ -2,7 +2,7 @@ module Gitlab
module SlashCommands
class IssueCommand < BaseCommand
def self.available?(project)
- project.issues_enabled? && project.default_issues_tracker?
+ project.issues_enabled?
end
def collection
diff --git a/lib/gitlab/untrusted_regexp.rb b/lib/gitlab/untrusted_regexp.rb
index 187a9e1145f..7ce2e9d636e 100644
--- a/lib/gitlab/untrusted_regexp.rb
+++ b/lib/gitlab/untrusted_regexp.rb
@@ -22,33 +22,9 @@ module Gitlab
end
def scan(text)
- text = text.dup # modified in-place
- results = []
-
- loop do
- match = scan_regexp.match(text)
- break unless match
-
- # Ruby scan returns empty strings, not nil
- groups = match.to_a.map(&:to_s)
-
- results <<
- if regexp.number_of_capturing_groups.zero?
- groups[0]
- else
- groups[1..-1]
- end
-
- matchsize = match.end(0)
-
- # No further matches
- break unless matchsize.present?
-
- text.slice!(0, matchsize)
- break unless text.present?
- end
-
- results
+ matches = scan_regexp.scan(text).to_a
+ matches.map!(&:first) if regexp.number_of_capturing_groups.zero?
+ matches
end
def replace(text, rewrite)
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index dba071d7e47..e0ac21305a5 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -40,14 +40,13 @@ module Gitlab
pages_domains: PagesDomain.count,
projects: Project.count,
projects_imported_from_github: Project.where(import_type: 'github').count,
- projects_prometheus_active: PrometheusService.active.count,
protected_branches: ProtectedBranch.count,
releases: Release.count,
snippets: Snippet.count,
todos: Todo.count,
uploads: Upload.count,
web_hooks: WebHook.count
- }
+ }.merge(services_usage)
}
end
@@ -64,6 +63,18 @@ module Gitlab
usage_data
end
+
+ def services_usage
+ types = {
+ JiraService: :projects_jira_active,
+ SlackService: :projects_slack_notifications_active,
+ SlackSlashCommandsService: :projects_slack_slash_active,
+ PrometheusService: :projects_prometheus_active
+ }
+
+ results = Service.unscoped.where(type: types.keys, active: true).group(:type).count
+ results.each_with_object({}) { |(key, value), response| response[types[key.to_sym]] = value }
+ end
end
end
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 8e91ee7287c..d9a5af09f08 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -37,8 +37,8 @@ module Gitlab
request_cache def can_create_tag?(ref)
return false unless can_access_git?
- if ProtectedTag.protected?(project, ref)
- project.protected_tags.protected_ref_accessible_to?(ref, user, action: :create)
+ if protected?(ProtectedTag, project, ref)
+ protected_tag_accessible_to?(ref, action: :create)
else
user.can?(:push_code, project)
end
@@ -47,20 +47,24 @@ module Gitlab
request_cache def can_delete_branch?(ref)
return false unless can_access_git?
- if ProtectedBranch.protected?(project, ref)
+ if protected?(ProtectedBranch, project, ref)
user.can?(:delete_protected_branch, project)
else
user.can?(:push_code, project)
end
end
+ def can_update_branch?(ref)
+ can_push_to_branch?(ref) || can_merge_to_branch?(ref)
+ end
+
request_cache def can_push_to_branch?(ref)
return false unless can_access_git?
- if ProtectedBranch.protected?(project, ref)
+ if protected?(ProtectedBranch, project, ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
- project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
+ protected_branch_accessible_to?(ref, action: :push)
else
user.can?(:push_code, project)
end
@@ -69,8 +73,8 @@ module Gitlab
request_cache def can_merge_to_branch?(ref)
return false unless can_access_git?
- if ProtectedBranch.protected?(project, ref)
- project.protected_branches.protected_ref_accessible_to?(ref, user, action: :merge)
+ if protected?(ProtectedBranch, project, ref)
+ protected_branch_accessible_to?(ref, action: :merge)
else
user.can?(:push_code, project)
end
@@ -87,5 +91,23 @@ module Gitlab
def can_access_git?
user && user.can?(:access_git)
end
+
+ def protected_branch_accessible_to?(ref, action:)
+ ProtectedBranch.protected_ref_accessible_to?(
+ ref, user,
+ action: action,
+ protected_refs: project.protected_branches)
+ end
+
+ def protected_tag_accessible_to?(ref, action:)
+ ProtectedTag.protected_ref_accessible_to?(
+ ref, user,
+ action: action,
+ protected_refs: project.protected_tags)
+ end
+
+ request_cache def protected?(kind, project, ref)
+ kind.protected?(project, ref)
+ end
end
end
diff --git a/lib/gitlab/workhorse.rb b/lib/gitlab/workhorse.rb
index 916ef365d78..3f25e463412 100644
--- a/lib/gitlab/workhorse.rb
+++ b/lib/gitlab/workhorse.rb
@@ -35,7 +35,10 @@ module Gitlab
when 'git_receive_pack'
Gitlab::GitalyClient.feature_enabled?(:post_receive_pack)
when 'git_upload_pack'
- Gitlab::GitalyClient.feature_enabled?(:post_upload_pack)
+ Gitlab::GitalyClient.feature_enabled?(
+ :post_upload_pack,
+ status: Gitlab::GitalyClient::MigrationStatus::OPT_OUT
+ )
when 'info_refs'
true
else
@@ -62,7 +65,7 @@ module Gitlab
end
def send_git_blob(repository, blob)
- params = if Gitlab::GitalyClient.feature_enabled?(:project_raw_show)
+ params = if Gitlab::GitalyClient.feature_enabled?(:workhorse_raw_show)
{
'GitalyServer' => gitaly_server_hash(repository),
'GetBlobRequest' => {
diff --git a/lib/tasks/migrate/setup_postgresql.rake b/lib/tasks/migrate/setup_postgresql.rake
index 4108cee08b4..9cc986535e1 100644
--- a/lib/tasks/migrate/setup_postgresql.rake
+++ b/lib/tasks/migrate/setup_postgresql.rake
@@ -4,6 +4,7 @@ require Rails.root.join('db/migrate/20151007120511_namespaces_projects_path_lowe
require Rails.root.join('db/migrate/20151008110232_add_users_lower_username_email_indexes')
require Rails.root.join('db/migrate/20161212142807_add_lower_path_index_to_routes')
require Rails.root.join('db/migrate/20170317203554_index_routes_path_for_like')
+require Rails.root.join('db/migrate/20170724214302_add_lower_path_index_to_redirect_routes')
require Rails.root.join('db/migrate/20170503185032_index_redirect_routes_path_for_like')
desc 'GitLab | Sets up PostgreSQL'
@@ -12,5 +13,6 @@ task setup_postgresql: :environment do
AddUsersLowerUsernameEmailIndexes.new.up
AddLowerPathIndexToRoutes.new.up
IndexRoutesPathForLike.new.up
+ AddLowerPathIndexToRedirectRoutes.new.up
IndexRedirectRoutesPathForLike.new.up
end
diff --git a/locale/ja/gitlab.po b/locale/ja/gitlab.po
index cf74abf81bc..04c61906c73 100644
--- a/locale/ja/gitlab.po
+++ b/locale/ja/gitlab.po
@@ -33,7 +33,8 @@ msgstr "%{commit_author_link}ã¯%{commit_timeago}å‰ã€ã‚³ãƒŸãƒƒãƒˆã—ã¾ã—ãŸã
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "%d 個ã®ãƒ‘イプライン"
+msgstr[0] "1 個ã®ãƒ‘イプライン"
+msgstr[1] "%d 個ã®ãƒ‘イプライン"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "CIã«ã¤ã„ã¦ã®ã‚°ãƒ©ãƒ•"
@@ -1135,8 +1136,7 @@ msgstr ""
msgid ""
"You are going to remove the fork relationship to source project "
"%{forked_from_project}. Are you ABSOLUTELY sure?"
-msgstr "å…ƒã®ãƒ—ロジェクト (%{forked_from_project}) ã¨ã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚\n"
-"本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
+msgstr "å…ƒã®ãƒ—ロジェクト (%{forked_from_project}) ã¨ã®ãƒªãƒ¬ãƒ¼ã‚·ãƒ§ãƒ³ã‚’削除ã—よã†ã¨ã—ã¦ã„ã¾ã™ã€‚本当ã«ã‚ˆã‚ã—ã„ã§ã™ã‹ï¼Ÿ"
msgid ""
"You are going to transfer %{project_name_with_namespace} to another owner. "
@@ -1201,4 +1201,3 @@ msgstr "メール通知"
msgid "parent"
msgid_plural "parents"
msgstr[0] "親"
-
diff --git a/locale/pt_BR/gitlab.po b/locale/pt_BR/gitlab.po
index c4918a4c920..78cf6d2dacc 100644
--- a/locale/pt_BR/gitlab.po
+++ b/locale/pt_BR/gitlab.po
@@ -6,13 +6,13 @@ msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-28 13:32+0200\n"
+"POT-Creation-Date: 2017-07-05 08:50-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-12 09:05-0400\n"
-"Last-Translator: Leandro Nunes dos Santos <leandronunes@gmail.com>\n"
"Language-Team: Portuguese (Brazil) (https://translate.zanata.org/project/view/GitLab)\n"
+"PO-Revision-Date: 2017-07-14 01:17-0400\n"
+"Last-Translator: Huang Tao <htve@outlook.com>\n"
"Language: pt-BR\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=2; plural=(n != 1)\n"
@@ -644,6 +644,12 @@ msgstr "Todos"
msgid "PipelineSchedules|Inactive"
msgstr "Inativo"
+msgid "PipelineSchedules|Input variable key"
+msgstr "PipelineSchedules|Chave da variável de entrada"
+
+msgid "PipelineSchedules|Input variable value"
+msgstr "PipelineSchedules|Valor da variável de entrada"
+
msgid "PipelineSchedules|Next Run"
msgstr "Próxima Execução"
@@ -653,12 +659,18 @@ msgstr "Nenhum"
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr "Digite uma descrição curta para esta pipeline"
+msgid "PipelineSchedules|Remove variable row"
+msgstr "PipelineSchedules|Remova a linha da variável"
+
msgid "PipelineSchedules|Take ownership"
msgstr "Tornar-se proprietário"
msgid "PipelineSchedules|Target"
msgstr "Destino"
+msgid "PipelineSchedules|Variables"
+msgstr "PipelineSchedules|Variáveis"
+
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "Personalizado"
@@ -1151,6 +1163,15 @@ msgid "Withdraw Access Request"
msgstr "Remover Requisição de Acesso"
msgid ""
+"You are going to remove %{group_name}.\n"
+"Removed groups CANNOT be restored!\n"
+"Are you ABSOLUTELY sure?"
+msgstr ""
+"Você vai remover %{group_name}.\n"
+"Grupos removidos NÃO PODEM ser restaurados!\n"
+"Você está ABSOLUTAMENTE certo?"
+
+msgid ""
"You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
diff --git a/locale/uk/gitlab.po b/locale/uk/gitlab.po
index 59a7eb6e1b3..2ded61f40d7 100644
--- a/locale/uk/gitlab.po
+++ b/locale/uk/gitlab.po
@@ -1,16 +1,16 @@
-# Ðндрей Витюк <andruwa13@gmail.com>, 2017. #zanata
# Huang Tao <htve@outlook.com>, 2017. #zanata
+# Ðндрей Витюк <andruwa13@gmail.com>, 2017. #zanata
msgid ""
msgstr ""
"Project-Id-Version: gitlab 1.0.0\n"
"Report-Msgid-Bugs-To: \n"
-"POT-Creation-Date: 2017-06-28 13:32+0200\n"
+"POT-Creation-Date: 2017-07-05 08:50-0500\n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: 8bit\n"
-"PO-Revision-Date: 2017-07-12 09:05-0400\n"
-"Last-Translator: Ðндрей Витюк <andruwa13@gmail.com>\n"
"Language-Team: Ukrainian (https://translate.zanata.org/project/view/GitLab)\n"
+"PO-Revision-Date: 2017-07-25 03:27-0400\n"
+"Last-Translator: Ðндрей Витюк <andruwa13@gmail.com>\n"
"Language: uk\n"
"X-Generator: Zanata 3.9.6\n"
"Plural-Forms: nplurals=3; plural=(n%10==1 && n%100!=11 ? 0 : n%10>=2 && "
@@ -57,7 +57,7 @@ msgid "Add Changelog"
msgstr "Додати ÑпиÑок змін (Changelog)"
msgid "Add Contribution guide"
-msgstr "Додати керівництво Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¸Ð±ÑƒÑ‚Ð¾Ñ€Ñ–Ð²"
+msgstr "Додати керівництво Ð´Ð»Ñ ÐºÐ¾Ð½Ñ‚Ñ€Ð¸Ð±â€™ÑŽÑ‚Ð¾Ñ€Ñ–Ð²"
msgid "Add License"
msgstr "Додати ліцензію"
@@ -209,7 +209,9 @@ msgstr[1] "Комміта"
msgstr[2] "Коммітів"
msgid "Commit duration in minutes for last 30 commits"
-msgstr "Комміт триваліÑÑ‚ÑŒ у хвилинах за оÑтанні 30 коммітів"
+msgstr ""
+"ТриваліÑÑ‚ÑŒ коммітів протÑгом декількох хвилин на протÑзі 30 оÑтанніх "
+"коммітів"
msgid "Commit message"
msgstr "Комміт повідомленнÑ"
@@ -236,10 +238,10 @@ msgid "Compare"
msgstr "ПорівнÑти"
msgid "Contribution guide"
-msgstr "Керівництво контрибуторів"
+msgstr "Керівництво контриб’юторів"
msgid "Contributors"
-msgstr "Контрибутори"
+msgstr "Контриб’ютори"
msgid "Copy URL to clipboard"
msgstr "Скопіювати URL в буфер обміну"
@@ -352,16 +354,16 @@ msgid "Download"
msgstr "Завантажити"
msgid "Download tar"
-msgstr "Завантажити в форматі tar"
+msgstr "Завантажити tar"
msgid "Download tar.bz2"
-msgstr "Завантажити в форматі tar.bz2"
+msgstr "Завантажити tar.bz2"
msgid "Download tar.gz"
-msgstr "Завантажити в форматі tar.gz"
+msgstr "Завантажити tar.gz"
msgid "Download zip"
-msgstr "Завантажити в форматі zip"
+msgstr "Завантажити zip"
msgid "DownloadArtifacts|Download"
msgstr "Завантажити"
@@ -379,7 +381,7 @@ msgid "Edit"
msgstr "Редагувати"
msgid "Edit Pipeline Schedule %{id}"
-msgstr "Редагувати Розклад Конвеєра % {id}"
+msgstr "Редагувати Розклад Конвеєра %{id}"
msgid "Every day (at 4:00am)"
msgstr "Кожен день (в 4:00 ранку)"
@@ -397,7 +399,7 @@ msgid "Failed to remove the pipeline schedule"
msgstr "Ðе вдалоÑÑ Ð²Ð¸Ð´Ð°Ð»Ð¸Ñ‚Ð¸ розклад Конвеєра"
msgid "Files"
-msgstr "Файли"
+msgstr "Файлів"
msgid "Filter by commit message"
msgstr "Фільтрувати Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½Ð½Ñ ÐºÐ¾Ð¼Ð¼Ñ–Ñ‚Ñ–Ð²"
@@ -436,7 +438,7 @@ msgid "GoToYourFork|Fork"
msgstr "Форк"
msgid "Home"
-msgstr "Початок"
+msgstr "Головна"
msgid "Housekeeping successfully started"
msgstr "ÐžÑ‡Ð¸Ñ‰ÐµÐ½Ð½Ñ ÑƒÑпішно розпочато"
@@ -451,13 +453,13 @@ msgid "Introducing Cycle Analytics"
msgstr "ПредÑтавлÑємо аналітику циклу"
msgid "Jobs for last month"
-msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð·Ð° оÑтанній міÑÑць"
+msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній міÑÑць"
msgid "Jobs for last week"
-msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð·Ð° оÑтанній тиждень"
+msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній тиждень"
msgid "Jobs for last year"
-msgstr "Ð—Ð°Ð²Ð´Ð°Ð½Ð½Ñ Ð·Ð° оÑтанній рік"
+msgstr "КількіÑÑ‚ÑŒ завдань за оÑтанній рік"
msgid "LFSStatus|Disabled"
msgstr "Вимкнено"
@@ -508,7 +510,7 @@ msgid "New Issue"
msgid_plural "New Issues"
msgstr[0] "Ðова проблема"
msgstr[1] "Ðові проблеми"
-msgstr[2] "Ðовах проблем"
+msgstr[2] "Ðових проблем"
msgid "New Pipeline Schedule"
msgstr "Ðовий розклад Конвеєра"
@@ -654,6 +656,12 @@ msgstr "Ð’ÑÑ–"
msgid "PipelineSchedules|Inactive"
msgstr "Ðеактивні"
+msgid "PipelineSchedules|Input variable key"
+msgstr "Введіть ім'Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¾Ñ—"
+
+msgid "PipelineSchedules|Input variable value"
+msgstr "Вхідні Ð·Ð½Ð°Ñ‡ÐµÐ½Ð½Ñ Ð·Ð¼Ñ–Ð½Ð½Ð¸Ñ…"
+
msgid "PipelineSchedules|Next Run"
msgstr "ÐаÑтупний запуÑк"
@@ -663,12 +671,18 @@ msgstr "Ðемає"
msgid "PipelineSchedules|Provide a short description for this pipeline"
msgstr "Задайте короткий Ð¾Ð¿Ð¸Ñ Ð´Ð»Ñ Ñ†ÑŒÐ¾Ð³Ð¾ Конвеєру"
+msgid "PipelineSchedules|Remove variable row"
+msgstr "Видалити змінні"
+
msgid "PipelineSchedules|Take ownership"
msgstr "Стати влаÑником"
msgid "PipelineSchedules|Target"
msgstr "Ціль"
+msgid "PipelineSchedules|Variables"
+msgstr "Змінні"
+
msgid "PipelineSheduleIntervalPattern|Custom"
msgstr "ВлаÑні"
@@ -745,7 +759,7 @@ msgid "ProjectLifecycle|Stage"
msgstr "Етап"
msgid "ProjectNetworkGraph|Graph"
-msgstr "Графік"
+msgstr "ІÑторіÑ"
msgid "Read more"
msgstr "Докладніше"
@@ -840,7 +854,7 @@ msgid "Source code"
msgstr "Код"
msgid "StarProject|Star"
-msgstr "Старт"
+msgstr "ПідпиÑатиÑÑ"
msgid "Start a %{new_merge_request} with these changes"
msgstr "Почати %{new_merge_request} з цих змін"
@@ -976,19 +990,19 @@ msgid "Timeago|%s minutes remaining"
msgstr "%s хвилини залишитиÑÑ"
msgid "Timeago|%s months ago"
-msgstr "%s міÑÑців тому"
+msgstr "%s міÑÑці(в) тому"
msgid "Timeago|%s months remaining"
-msgstr "%s міÑÑці, що залишилиÑÑ"
+msgstr "%s міÑÑці(в), що залишилиÑÑ"
msgid "Timeago|%s seconds remaining"
msgstr "%s Ñекунд, що залишаютьÑÑ"
msgid "Timeago|%s weeks ago"
-msgstr "%s тижнів тому"
+msgstr "%s тижні(в) тому"
msgid "Timeago|%s weeks remaining"
-msgstr "%s тижнів залишилиÑÑ"
+msgstr "%s тижні(в) залишилиÑÑ"
msgid "Timeago|%s years ago"
msgstr "%s років тому"
@@ -1018,7 +1032,7 @@ msgid "Timeago|Past due"
msgstr "ПроÑтрочені"
msgid "Timeago|a day ago"
-msgstr "годин тому"
+msgstr "День тому"
msgid "Timeago|a month ago"
msgstr "міÑÑць тому"
@@ -1042,28 +1056,28 @@ msgid "Timeago|about an hour ago"
msgstr "Близько години тому"
msgid "Timeago|in %s days"
-msgstr "через %s днїв"
+msgstr "через %s дні(в)"
msgid "Timeago|in %s hours"
-msgstr "через %s години"
+msgstr "через %s годин(и)"
msgid "Timeago|in %s minutes"
-msgstr "через %s хвилини"
+msgstr "через %s хвилин(и)"
msgid "Timeago|in %s months"
-msgstr "через %s міÑÑців"
+msgstr "через %s міÑÑці(в)"
msgid "Timeago|in %s seconds"
-msgstr "через %s Ñекунд"
+msgstr "через %s Ñекунд(и)"
msgid "Timeago|in %s weeks"
-msgstr "через %s тижні"
+msgstr "через %s тижні(в)"
msgid "Timeago|in %s years"
-msgstr "через %s років"
+msgstr "через %s роки(ів)"
msgid "Timeago|in 1 day"
-msgstr "через день"
+msgstr "через 1 день"
msgid "Timeago|in 1 hour"
msgstr "через годину"
@@ -1081,22 +1095,22 @@ msgid "Timeago|in 1 year"
msgstr "через рік"
msgid "Timeago|less than a minute ago"
-msgstr "менш хвилини тому"
+msgstr "менше хвилини тому"
msgid "Time|hr"
msgid_plural "Time|hrs"
-msgstr[0] "Година"
-msgstr[1] "Годині"
-msgstr[2] "Годин"
+msgstr[0] "година"
+msgstr[1] "години"
+msgstr[2] "годин"
msgid "Time|min"
msgid_plural "Time|mins"
msgstr[0] "хвилина"
-msgstr[1] "хвилині"
+msgstr[1] "хвилини"
msgstr[2] "хвилин"
msgid "Time|s"
-msgstr "Ñекунда"
+msgstr "Ñекунд(а)"
msgid "Total Time"
msgstr "Загальний чаÑ"
@@ -1105,7 +1119,7 @@ msgid "Total test time for all commits/merges"
msgstr "Загальний чаÑ, щоб перевірити вÑÑ– фікÑації/злиттÑ"
msgid "Unstar"
-msgstr "ЗнÑти позначку"
+msgstr "ВідпиÑатиÑÑŒ"
msgid "Upload New File"
msgstr "Завантажити новий файл"
@@ -1117,7 +1131,7 @@ msgid "UploadLink|click to upload"
msgstr "ÐатиÑніть, щоб завантажити"
msgid "Use your global notification setting"
-msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ð¸Ð¹ Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
+msgstr "ВикориÑтовуютьÑÑ Ð³Ð»Ð¾Ð±Ð°Ð»ÑŒÐ½Ñ– Ð½Ð°Ð»Ð°ÑˆÑ‚ÑƒÐ²Ð°Ð½Ð½Ñ Ð¿Ð¾Ð²Ñ–Ð´Ð¾Ð¼Ð»ÐµÐ½ÑŒ"
msgid "View open merge request"
msgstr "ПереглÑд відкритих запитів на злиттÑ"
@@ -1141,6 +1155,15 @@ msgid "Withdraw Access Request"
msgstr "СкаÑувати запит доÑтупу"
msgid ""
+"You are going to remove %{group_name}.\n"
+"Removed groups CANNOT be restored!\n"
+"Are you ABSOLUTELY sure?"
+msgstr ""
+"Ви хочете видалити %{group_name}.\n"
+"Видалені групи ÐЕ МОЖÐРбуду відновити!\n"
+"Ви ÐБСОЛЮТÐО впевнені?"
+
+msgid ""
"You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
@@ -1231,4 +1254,3 @@ msgid_plural "parents"
msgstr[0] "джерело"
msgstr[1] "джерела"
msgstr[2] "джерел"
-
diff --git a/locale/zh_CN/gitlab.po b/locale/zh_CN/gitlab.po
index 47b72d7be1a..f471e7def25 100644
--- a/locale/zh_CN/gitlab.po
+++ b/locale/zh_CN/gitlab.po
@@ -29,7 +29,8 @@ msgstr "ç”± %{commit_author_link} æ交于 %{commit_timeago}"
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "%d æ¡æµæ°´çº¿"
+msgstr[0] "1 æ¡æµæ°´çº¿"
+msgstr[1] "%d æ¡æµæ°´çº¿"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒç»­é›†æˆæ•°æ®å›¾"
@@ -236,7 +237,7 @@ msgstr "创建新目录"
msgid ""
"Create a personal access token on your account to pull or push via "
"%{protocol}."
-msgstr "在å¸æˆ·ä¸Šåˆ›å»ºä¸ªäººè®¿é—®ä»¤ç‰Œï¼Œä»¥é€šè¿‡ï¼…{protocol}æ¥æ‹‰å–或推é€ã€‚"
+msgstr "在å¸æˆ·ä¸Šåˆ›å»ºä¸ªäººè®¿é—®ä»¤ç‰Œï¼Œä»¥é€šè¿‡ %{protocol} æ¥æ‹‰å–或推é€ã€‚"
msgid "Create directory"
msgstr "创建目录"
@@ -1109,9 +1110,7 @@ msgid ""
"You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
-msgstr "å³å°†è¦åˆ é™¤ %{project_name_with_namespace}。\n"
-"已删除的项目无法æ¢å¤ï¼\n"
-"确定继续å—?"
+msgstr "å³å°†è¦åˆ é™¤ %{project_name_with_namespace}。已删除的项目无法æ¢å¤ï¼ç¡®å®šç»§ç»­å—?"
msgid ""
"You are going to remove the fork relationship to source project "
@@ -1179,4 +1178,3 @@ msgstr "通知邮件"
msgid "parent"
msgid_plural "parents"
msgstr[0] "父级"
-
diff --git a/locale/zh_HK/gitlab.po b/locale/zh_HK/gitlab.po
index 8a4e6da4ea9..1b7c39f8f62 100644
--- a/locale/zh_HK/gitlab.po
+++ b/locale/zh_HK/gitlab.po
@@ -28,7 +28,8 @@ msgstr "ç”± %{commit_author_link} æ交於 %{commit_timeago}"
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "%d æ¢æµæ°´ç·š"
+msgstr[0] "1 æ¢æµæ°´ç·š"
+msgstr[1] "%d æ¢æµæ°´ç·š"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "相關æŒçºŒé›†æˆçš„圖åƒé›†åˆ"
@@ -1108,9 +1109,7 @@ msgid ""
"You are going to remove %{project_name_with_namespace}.\n"
"Removed project CANNOT be restored!\n"
"Are you ABSOLUTELY sure?"
-msgstr "å³å°‡è¦åˆªé™¤ %{project_name_with_namespace}。\n"
-"已刪除的項目無法æ¢è¤‡ï¼\n"
-"確定繼續嗎?"
+msgstr "å³å°‡è¦åˆªé™¤ %{project_name_with_namespace}。已刪除的項目無法æ¢è¤‡ï¼ç¢ºå®šç¹¼çºŒå—Žï¼Ÿ"
msgid ""
"You are going to remove the fork relationship to source project "
@@ -1178,4 +1177,3 @@ msgstr "通知郵件"
msgid "parent"
msgid_plural "parents"
msgstr[0] "父級"
-
diff --git a/locale/zh_TW/gitlab.po b/locale/zh_TW/gitlab.po
index 05173ed12c0..8d30a78145d 100644
--- a/locale/zh_TW/gitlab.po
+++ b/locale/zh_TW/gitlab.po
@@ -32,7 +32,8 @@ msgstr "%{commit_author_link} 在 %{commit_timeago} é€äº¤"
msgid "1 pipeline"
msgid_plural "%d pipelines"
-msgstr[0] "%d æ¢æµæ°´ç·š"
+msgstr[0] "1 æ¢æµæ°´ç·š"
+msgstr[1] "%d æ¢æµæ°´ç·š"
msgid "A collection of graphs regarding Continuous Integration"
msgstr "æŒçºŒæ•´åˆ (CI) 相關的圖表"
@@ -1193,4 +1194,3 @@ msgstr "通知信"
msgid "parent"
msgid_plural "parents"
msgstr[0] "上層"
-
diff --git a/spec/controllers/admin/dashboard_controller_spec.rb b/spec/controllers/admin/dashboard_controller_spec.rb
new file mode 100644
index 00000000000..6eb9f7867d5
--- /dev/null
+++ b/spec/controllers/admin/dashboard_controller_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe Admin::DashboardController do
+ describe '#index' do
+ context 'with pending_delete projects' do
+ render_views
+
+ it 'does not retrieve projects that are pending deletion' do
+ sign_in(create(:admin))
+
+ project = create(:project)
+ pending_delete_project = create(:project, pending_delete: true)
+
+ get :index
+
+ expect(response.body).to match(project.name)
+ expect(response.body).not_to match(pending_delete_project.name)
+ end
+ end
+ end
+end
diff --git a/spec/controllers/projects/issues_controller_spec.rb b/spec/controllers/projects/issues_controller_spec.rb
index 18d0be3c103..2c57f3bcf8d 100644
--- a/spec/controllers/projects/issues_controller_spec.rb
+++ b/spec/controllers/projects/issues_controller_spec.rb
@@ -7,16 +7,30 @@ describe Projects::IssuesController do
describe "GET #index" do
context 'external issue tracker' do
- let!(:service) do
- create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker', project_url: 'http://test.com')
+ before do
+ sign_in(user)
+ project.add_developer(user)
+ create(:jira_service, project: project)
end
- it 'redirects to the external issue tracker' do
- controller.instance_variable_set(:@project, project)
+ context 'when GitLab issues disabled' do
+ it 'returns 404 status' do
+ project.issues_enabled = false
+ project.save!
- get :index, namespace_id: project.namespace, project_id: project
+ get :index, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when GitLab issues enabled' do
+ it 'renders the "index" template' do
+ get :index, namespace_id: project.namespace, project_id: project
- expect(response).to redirect_to(service.issue_tracker_path)
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:index)
+ end
end
end
@@ -42,15 +56,7 @@ describe Projects::IssuesController do
it "returns 404 when issues are disabled" do
project.issues_enabled = false
- project.save
-
- get :index, namespace_id: project.namespace, project_id: project
- expect(response).to have_http_status(404)
- end
-
- it "returns 404 when external issue tracker is enabled" do
- controller.instance_variable_set(:@project, project)
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ project.save!
get :index, namespace_id: project.namespace, project_id: project
expect(response).to have_http_status(404)
@@ -148,14 +154,29 @@ describe Projects::IssuesController do
before do
sign_in(user)
project.team << [user, :developer]
+
+ external = double
+ allow(project).to receive(:external_issue_tracker).and_return(external)
end
- it 'redirects to the external issue tracker' do
- controller.instance_variable_set(:@project, project)
+ context 'when GitLab issues disabled' do
+ it 'returns 404 status' do
+ project.issues_enabled = false
+ project.save!
- get :new, namespace_id: project.namespace, project_id: project
+ get :new, namespace_id: project.namespace, project_id: project
- expect(response).to redirect_to('http://test.com')
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when GitLab issues enabled' do
+ it 'renders the "new" template' do
+ get :new, namespace_id: project.namespace, project_id: project
+
+ expect(response).to have_http_status(200)
+ expect(response).to render_template(:new)
+ end
end
end
end
@@ -806,7 +827,7 @@ describe Projects::IssuesController do
delete :destroy, namespace_id: project.namespace, project_id: project, id: issue.iid
expect(response).to have_http_status(302)
- expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./).now
+ expect(controller).to set_flash[:notice].to(/The issue was successfully deleted\./)
end
it 'delegates the update of the todos count cache to TodoService' do
diff --git a/spec/controllers/projects/jobs_controller_spec.rb b/spec/controllers/projects/jobs_controller_spec.rb
index 472e5fc51a0..5a295ae47a6 100644
--- a/spec/controllers/projects/jobs_controller_spec.rb
+++ b/spec/controllers/projects/jobs_controller_spec.rb
@@ -7,6 +7,10 @@ describe Projects::JobsController do
let(:pipeline) { create(:ci_pipeline, project: project) }
let(:user) { create(:user) }
+ before do
+ stub_not_protect_default_branch
+ end
+
describe 'GET index' do
context 'when scope is pending' do
before do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index c193babead0..2fce4b7a85f 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -439,7 +439,7 @@ describe Projects::MergeRequestsController do
delete :destroy, namespace_id: project.namespace, project_id: project, id: merge_request.iid
expect(response).to have_http_status(302)
- expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./).now
+ expect(controller).to set_flash[:notice].to(/The merge request was successfully deleted\./)
end
it 'delegates the update of the todos count cache to TodoService' do
diff --git a/spec/controllers/projects/pipelines_controller_spec.rb b/spec/controllers/projects/pipelines_controller_spec.rb
index 734532668d3..c8de275ca3e 100644
--- a/spec/controllers/projects/pipelines_controller_spec.rb
+++ b/spec/controllers/projects/pipelines_controller_spec.rb
@@ -8,6 +8,7 @@ describe Projects::PipelinesController do
let(:feature) { ProjectFeature::DISABLED }
before do
+ stub_not_protect_default_branch
project.add_developer(user)
project.project_feature.update(
builds_access_level: feature)
@@ -158,7 +159,7 @@ describe Projects::PipelinesController do
context 'when builds are enabled' do
let(:feature) { ProjectFeature::ENABLED }
-
+
it 'retries a pipeline without returning any content' do
expect(response).to have_http_status(:no_content)
expect(build.reload).to be_retried
@@ -175,7 +176,7 @@ describe Projects::PipelinesController do
describe 'POST cancel.json' do
let!(:pipeline) { create(:ci_pipeline, project: project) }
let!(:build) { create(:ci_build, :running, pipeline: pipeline) }
-
+
before do
post :cancel, namespace_id: project.namespace,
project_id: project,
@@ -185,7 +186,7 @@ describe Projects::PipelinesController do
context 'when builds are enabled' do
let(:feature) { ProjectFeature::ENABLED }
-
+
it 'cancels a pipeline without returning any content' do
expect(response).to have_http_status(:no_content)
expect(pipeline.reload).to be_canceled
diff --git a/spec/controllers/sent_notifications_controller_spec.rb b/spec/controllers/sent_notifications_controller_spec.rb
index 7340a4e16c0..c8771eda313 100644
--- a/spec/controllers/sent_notifications_controller_spec.rb
+++ b/spec/controllers/sent_notifications_controller_spec.rb
@@ -23,7 +23,7 @@ describe SentNotificationsController, type: :controller do
end
it 'sets the flash message' do
- expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ expect(controller).to set_flash[:notice].to(/unsubscribed/)
end
it 'redirects to the login page' do
@@ -83,7 +83,7 @@ describe SentNotificationsController, type: :controller do
end
it 'sets the flash message' do
- expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ expect(controller).to set_flash[:notice].to(/unsubscribed/)
end
it 'redirects to the issue page' do
@@ -109,7 +109,7 @@ describe SentNotificationsController, type: :controller do
end
it 'sets the flash message' do
- expect(controller).to set_flash[:notice].to(/unsubscribed/).now
+ expect(controller).to set_flash[:notice].to(/unsubscribed/)
end
it 'redirects to the merge request page' do
diff --git a/spec/features/admin/admin_appearance_spec.rb b/spec/features/admin/admin_appearance_spec.rb
index b9e361328df..2f90f668e89 100644
--- a/spec/features/admin/admin_appearance_spec.rb
+++ b/spec/features/admin/admin_appearance_spec.rb
@@ -63,11 +63,11 @@ feature 'Admin Appearance', feature: true do
end
def logo_selector
- '//img[@src^="/uploads/-/system/appearance/logo"]'
+ '//img[data-src^="/uploads/-/system/appearance/logo"]'
end
def header_logo_selector
- '//img[@src^="/uploads/-/system/appearance/header_logo"]'
+ '//img[data-src^="/uploads/-/system/appearance/header_logo"]'
end
def logo_fixture
diff --git a/spec/features/dashboard/issues_spec.rb b/spec/features/dashboard/issues_spec.rb
index 69c1a2ed89a..2a5ef08da60 100644
--- a/spec/features/dashboard/issues_spec.rb
+++ b/spec/features/dashboard/issues_spec.rb
@@ -78,5 +78,23 @@ RSpec.describe 'Dashboard Issues', feature: true do
expect(page).not_to have_content(project_with_issues_disabled.name_with_namespace)
end
end
+
+ it 'shows the new issue page', js: true do
+ Gitlab::Application.routes.default_url_options = {
+ host: Capybara.current_session.server.host,
+ port: Capybara.current_session.server.port,
+ protocol: 'http'
+ }
+
+ find('.new-project-item-select-button').trigger('click')
+ wait_for_requests
+ find('.select2-results li').click
+
+ expect(page).to have_current_path("/#{project.path_with_namespace}/issues/new")
+
+ page.within('#content-body') do
+ expect(page).to have_selector('.issue-form')
+ end
+ end
end
end
diff --git a/spec/features/explore/new_menu_spec.rb b/spec/features/explore/new_menu_spec.rb
index 7dd69f550ac..e51d527bdf9 100644
--- a/spec/features/explore/new_menu_spec.rb
+++ b/spec/features/explore/new_menu_spec.rb
@@ -1,17 +1,13 @@
require 'spec_helper'
feature 'Top Plus Menu', feature: true, js: true do
- let(:user) { create :user }
- let(:guest_user) { create :user}
+ let(:user) { create(:user) }
let(:group) { create(:group) }
let(:project) { create(:project, :repository, creator: user, namespace: user.namespace) }
let(:public_project) { create(:project, :public) }
before do
group.add_owner(user)
- group.add_guest(guest_user)
-
- project.add_guest(guest_user)
end
context 'used by full user' do
@@ -39,7 +35,7 @@ feature 'Top Plus Menu', feature: true, js: true do
scenario 'click on New snippet shows new snippet page' do
visit root_dashboard_path
-
+
click_topmenuitem("New snippet")
expect(page).to have_content('New Snippet')
@@ -102,7 +98,12 @@ feature 'Top Plus Menu', feature: true, js: true do
end
context 'used by guest user' do
+ let(:guest_user) { create(:user) }
+
before do
+ group.add_guest(guest_user)
+ project.add_guest(guest_user)
+
sign_in(guest_user)
end
@@ -153,7 +154,7 @@ feature 'Top Plus Menu', feature: true, js: true do
scenario 'has no New project for group menu item' do
visit group_path(group)
-
+
expect(find('.header-new.dropdown')).not_to have_selector('.header-new-group-project')
end
end
@@ -168,5 +169,5 @@ feature 'Top Plus Menu', feature: true, js: true do
def hasnot_topmenuitem(item_name)
expect(find('.header-new.dropdown')).not_to have_content(item_name)
- end
+ end
end
diff --git a/spec/features/issuables/markdown_references_spec.rb b/spec/features/issuables/markdown_references_spec.rb
new file mode 100644
index 00000000000..f51b2e4001a
--- /dev/null
+++ b/spec/features/issuables/markdown_references_spec.rb
@@ -0,0 +1,193 @@
+require 'rails_helper'
+
+describe 'Markdown References', :feature, :js do
+ let(:user) { create(:user) }
+ let(:actual_project) { create(:project, :public) }
+ let(:merge_request) { create(:merge_request, target_project: actual_project, source_project: actual_project)}
+ let(:issue_actual_project) { create(:issue, project: actual_project) }
+ let!(:other_project) { create(:empty_project, :public) }
+ let!(:issue_other_project) { create(:issue, project: other_project) }
+ let(:issues) { [issue_actual_project, issue_other_project] }
+
+ def build_note
+ markdown = "Referencing internal issue #{issue_actual_project.to_reference}, " +
+ "cross-project #{issue_other_project.to_reference(actual_project)} external JIRA-5 " +
+ "and non existing #999"
+
+ page.within('#diff-notes-app') do
+ fill_in 'note_note', with: markdown
+ end
+ end
+
+ shared_examples 'correct references' do
+ before do
+ remotelink = double(:remotelink, all: [], build: double(save!: true))
+
+ stub_request(:get, "https://jira.example.com/rest/api/2/issue/JIRA-5")
+ stub_request(:post, "https://jira.example.com/rest/api/2/issue/JIRA-5/comment")
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:remotelink).and_return(remotelink)
+
+ sign_in(user)
+ visit merge_request_path(merge_request)
+ build_note
+ end
+
+ def links_expectations
+ issues.each do |issue|
+ if referenced_issues.include?(issue)
+ expect(page).to have_link(issue.to_reference, href: issue_path(issue))
+ else
+ expect(page).not_to have_link(issue.to_reference, href: issue_path(issue))
+ end
+ end
+
+ if jira_referenced
+ expect(page).to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
+ else
+ expect(page).not_to have_link('JIRA-5', href: 'https://jira.example.com/browse/JIRA-5')
+ end
+
+ expect(page).not_to have_link('#999')
+ end
+
+ it 'creates a link to the referenced issue on the preview' do
+ find('.js-md-preview-button').click
+ wait_for_requests
+
+ page.within('.md-preview-holder') do
+ links_expectations
+ end
+ end
+
+ it 'creates a link to the referenced issue after submit' do
+ click_button 'Comment'
+ wait_for_requests
+
+ page.within('#diff-notes-app') do
+ links_expectations
+ end
+ end
+
+ it 'creates a note on the referenced issues' do
+ click_button 'Comment'
+ wait_for_requests
+
+ if referenced_issues.include?(issue_actual_project)
+ visit issue_path(issue_actual_project)
+
+ page.within('#notes') do
+ expect(page).to have_content(
+ "#{user.to_reference} mentioned in merge request #{merge_request.to_reference}"
+ )
+ end
+ end
+
+ if referenced_issues.include?(issue_other_project)
+ visit issue_path(issue_other_project)
+
+ page.within('#notes') do
+ expect(page).to have_content(
+ "#{user.to_reference} mentioned in merge request #{merge_request.to_reference(other_project)}"
+ )
+ end
+ end
+ end
+ end
+
+ context 'when internal issues tracker is enabled for the other project' do
+ context 'when only internal issues tracker is enabled for the actual project' do
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_actual_project, issue_other_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+
+ context 'when both external and internal issues trackers are enabled for the actual project' do
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_actual_project, issue_other_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context 'when only external issues tracker is enabled for the actual project' do
+ before do
+ create(:jira_service, project: actual_project)
+
+ actual_project.issues_enabled = false
+ actual_project.save!
+ end
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_other_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context 'when no tracker is enabled for the actual project' do
+ before do
+ actual_project.issues_enabled = false
+ actual_project.save!
+ end
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_other_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+ end
+
+ context 'when internal issues tracker is disabled for the other project' do
+ before do
+ other_project.issues_enabled = false
+ other_project.save!
+ end
+
+ context 'when only internal issues tracker is enabled for the actual project' do
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_actual_project] }
+ let(:jira_referenced) { false }
+ end
+ end
+
+ context 'when both external and internal issues trackers are enabled for the actual project' do
+ before do
+ create(:jira_service, project: actual_project)
+ end
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [issue_actual_project] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context 'when only external issues tracker is enabled for the actual project' do
+ before do
+ create(:jira_service, project: actual_project)
+
+ actual_project.issues_enabled = false
+ actual_project.save!
+ end
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [] }
+ let(:jira_referenced) { true }
+ end
+ end
+
+ context 'when no issues tracker is enabled for the actual project' do
+ before do
+ actual_project.issues_enabled = false
+ actual_project.save!
+ end
+
+ include_examples 'correct references' do
+ let(:referenced_issues) { [] }
+ let(:jira_referenced) { false }
+ end
+ end
+ end
+end
diff --git a/spec/features/issues/user_uses_slash_commands_spec.rb b/spec/features/issues/user_uses_slash_commands_spec.rb
index 4740402dc01..0c3c27e3e45 100644
--- a/spec/features/issues/user_uses_slash_commands_spec.rb
+++ b/spec/features/issues/user_uses_slash_commands_spec.rb
@@ -118,5 +118,42 @@ feature 'Issues > User uses quick actions', feature: true, js: true do
expect(page).not_to have_content '/wip'
end
end
+
+ describe 'mark issue as duplicate' do
+ let(:issue) { create(:issue, project: project) }
+ let(:original_issue) { create(:issue, project: project) }
+
+ context 'when the current user can update issues' do
+ it 'does not create a note, and marks the issue as a duplicate' do
+ write_note("/duplicate ##{original_issue.to_reference}")
+
+ expect(page).not_to have_content "/duplicate #{original_issue.to_reference}"
+ expect(page).to have_content 'Commands applied'
+ expect(page).to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
+
+ expect(issue.reload).to be_closed
+ end
+ end
+
+ context 'when the current user cannot update the issue' do
+ let(:guest) { create(:user) }
+ before do
+ project.team << [guest, :guest]
+ gitlab_sign_out
+ sign_in(guest)
+ visit project_issue_path(project, issue)
+ end
+
+ it 'does not create a note, and does not mark the issue as a duplicate' do
+ write_note("/duplicate ##{original_issue.to_reference}")
+
+ expect(page).to have_content "/duplicate ##{original_issue.to_reference}"
+ expect(page).not_to have_content 'Commands applied'
+ expect(page).not_to have_content "marked this issue as a duplicate of #{original_issue.to_reference}"
+
+ expect(issue.reload).to be_open
+ end
+ end
+ end
end
end
diff --git a/spec/features/markdown_spec.rb b/spec/features/markdown_spec.rb
index 534be3ab5a7..1aca3e3a9fd 100644
--- a/spec/features/markdown_spec.rb
+++ b/spec/features/markdown_spec.rb
@@ -100,7 +100,7 @@ describe 'GitLab Markdown', feature: true do
end
it 'permits img elements' do
- expect(doc).to have_selector('img[src*="smile.png"]')
+ expect(doc).to have_selector('img[data-src*="smile.png"]')
end
it 'permits br elements' do
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 827e02a58d0..1588f8a828a 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -39,14 +39,25 @@ describe 'Edit Project Settings', feature: true do
end
end
- context "When external issue tracker is enabled" do
- it "does not hide issues tab" do
- project.project_feature.update(issues_access_level: ProjectFeature::DISABLED)
+ context 'When external issue tracker is enabled and issues enabled on project settings' do
+ it 'does not hide issues tab' do
allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
visit project_path(project)
- expect(page).to have_selector(".shortcuts-issues")
+ expect(page).to have_selector('.shortcuts-issues')
+ end
+ end
+
+ context 'When external issue tracker is enabled and issues disabled on project settings' do
+ it 'hides issues tab' do
+ project.issues_enabled = false
+ project.save!
+ allow_any_instance_of(Project).to receive(:external_issue_tracker).and_return(JiraService.new)
+
+ visit project_path(project)
+
+ expect(page).not_to have_selector('.shortcuts-issues')
end
end
diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb
index 7c29af247d6..b71eec0ecfd 100644
--- a/spec/features/projects/services/jira_service_spec.rb
+++ b/spec/features/projects/services/jira_service_spec.rb
@@ -6,17 +6,12 @@ feature 'Setup Jira service', :feature, :js do
let(:service) { project.create_jira_service }
let(:url) { 'http://jira.example.com' }
-
- def stub_project_url
- WebMock.stub_request(:get, 'http://jira.example.com/rest/api/2/project/GitLabProject')
- .with(basic_auth: %w(username password))
- end
+ let(:test_url) { 'http://jira.example.com/rest/api/2/serverInfo' }
def fill_form(active = true)
check 'Active' if active
fill_in 'service_url', with: url
- fill_in 'service_project_key', with: 'GitLabProject'
fill_in 'service_username', with: 'username'
fill_in 'service_password', with: 'password'
fill_in 'service_jira_issue_transition_id', with: '25'
@@ -31,11 +26,10 @@ feature 'Setup Jira service', :feature, :js do
describe 'user sets and activates Jira Service' do
context 'when Jira connection test succeeds' do
- before do
- stub_project_url
- end
-
it 'activates the JIRA service' do
+ server_info = { key: 'value' }.to_json
+ WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password)).to_return(body: server_info)
+
click_link('JIRA')
fill_form
click_button('Test settings and save changes')
@@ -47,10 +41,6 @@ feature 'Setup Jira service', :feature, :js do
end
context 'when Jira connection test fails' do
- before do
- stub_project_url.to_return(status: 401)
- end
-
it 'shows errors when some required fields are not filled in' do
click_link('JIRA')
@@ -64,6 +54,9 @@ feature 'Setup Jira service', :feature, :js do
end
it 'activates the JIRA service' do
+ WebMock.stub_request(:get, test_url).with(basic_auth: %w(username password))
+ .to_raise(JIRA::HTTPError.new(double(message: 'message')))
+
click_link('JIRA')
fill_form
click_button('Test settings and save changes')
diff --git a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
index 5843f18d89f..8188d4c79f4 100644
--- a/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_group_spec.rb
@@ -18,7 +18,7 @@ feature 'User uploads avatar to group', feature: true do
visit group_path(group)
- expect(page).to have_selector(%Q(img[src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"]))
+ expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/group/avatar/#{group.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(group.reload.avatar.file).to exist
diff --git a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
index e8171dcaeb0..2628508afe8 100644
--- a/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
+++ b/spec/features/uploads/user_uploads_avatar_to_profile_spec.rb
@@ -16,7 +16,7 @@ feature 'User uploads avatar to profile', feature: true do
visit user_path(user)
- expect(page).to have_selector(%Q(img[src$="/uploads/-/system/user/avatar/#{user.id}/dk.png"]))
+ expect(page).to have_selector(%Q(img[data-src$="/uploads/-/system/user/avatar/#{user.id}/dk.png"]))
# Cheating here to verify something that isn't user-facing, but is important
expect(user.reload.avatar.file).to exist
diff --git a/spec/finders/admin/projects_finder_spec.rb b/spec/finders/admin/projects_finder_spec.rb
new file mode 100644
index 00000000000..296d6c51d04
--- /dev/null
+++ b/spec/finders/admin/projects_finder_spec.rb
@@ -0,0 +1,136 @@
+require 'spec_helper'
+
+describe Admin::ProjectsFinder do
+ describe '#execute' do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :public) }
+
+ let!(:private_project) do
+ create(:empty_project, :private, name: 'A', path: 'A')
+ end
+
+ let!(:internal_project) do
+ create(:empty_project, :internal, group: group, name: 'B', path: 'B')
+ end
+
+ let!(:public_project) do
+ create(:empty_project, :public, group: group, name: 'C', path: 'C')
+ end
+
+ let!(:shared_project) do
+ create(:empty_project, :private, name: 'D', path: 'D')
+ end
+
+ let(:params) { {} }
+ let(:current_user) { user }
+ let(:project_ids_relation) { nil }
+ let(:finder) { described_class.new(params: params, current_user: current_user) }
+
+ subject { finder.execute.to_a }
+
+ context 'without a user' do
+ let(:current_user) { nil }
+
+ it { is_expected.to match_array([shared_project, public_project, internal_project, private_project]) }
+ end
+
+ context 'with a user' do
+ it { is_expected.to match_array([shared_project, public_project, internal_project, private_project]) }
+ end
+
+ context 'filter by namespace_id' do
+ let(:namespace) { create(:namespace) }
+ let!(:project_in_namespace) { create(:empty_project, namespace: namespace) }
+ let(:params) { { namespace_id: namespace.id } }
+
+ it { is_expected.to eq([project_in_namespace]) }
+ end
+
+ context 'filter by visibility_level' do
+ before do
+ private_project.add_master(user)
+ end
+
+ context 'private' do
+ let(:params) { { visibility_level: Gitlab::VisibilityLevel::PRIVATE } }
+
+ it { is_expected.to match_array([shared_project, private_project]) }
+ end
+
+ context 'internal' do
+ let(:params) { { visibility_level: Gitlab::VisibilityLevel::INTERNAL } }
+
+ it { is_expected.to eq([internal_project]) }
+ end
+
+ context 'public' do
+ let(:params) { { visibility_level: Gitlab::VisibilityLevel::PUBLIC } }
+
+ it { is_expected.to eq([public_project]) }
+ end
+ end
+
+ context 'filter by push' do
+ let(:pushed_event) { create(:event, :pushed) }
+ let!(:project_with_push) { pushed_event.project }
+ let(:params) { { with_push: true } }
+
+ it { is_expected.to eq([project_with_push]) }
+ end
+
+ context 'filter by abandoned' do
+ before do
+ private_project.update(last_activity_at: Time.zone.now - 6.months - 1.minute)
+ end
+
+ let(:params) { { abandoned: true } }
+
+ it { is_expected.to eq([private_project]) }
+ end
+
+ context 'filter by last_repository_check_failed' do
+ before do
+ private_project.update(last_repository_check_failed: true)
+ end
+
+ let(:params) { { last_repository_check_failed: true } }
+
+ it { is_expected.to eq([private_project]) }
+ end
+
+ context 'filter by archived' do
+ let!(:archived_project) { create(:empty_project, :public, :archived, name: 'E', path: 'E') }
+
+ context 'archived=false' do
+ let(:params) { { archived: false } }
+
+ it { is_expected.to match_array([shared_project, public_project, internal_project, private_project]) }
+ end
+
+ context 'archived=true' do
+ let(:params) { { archived: true } }
+
+ it { is_expected.to match_array([archived_project, shared_project, public_project, internal_project, private_project]) }
+ end
+ end
+
+ context 'filter by personal' do
+ let!(:personal_project) { create(:empty_project, namespace: user.namespace) }
+ let(:params) { { personal: true } }
+
+ it { is_expected.to eq([personal_project]) }
+ end
+
+ context 'filter by name' do
+ let(:params) { { name: 'C' } }
+
+ it { is_expected.to match_array([shared_project, public_project, private_project]) }
+ end
+
+ context 'sorting' do
+ let(:params) { { sort: 'name_asc' } }
+
+ it { is_expected.to eq([private_project, internal_project, public_project, shared_project]) }
+ end
+ end
+end
diff --git a/spec/helpers/application_helper_spec.rb b/spec/helpers/application_helper_spec.rb
index f5e139685e8..ac5a58ac189 100644
--- a/spec/helpers/application_helper_spec.rb
+++ b/spec/helpers/application_helper_spec.rb
@@ -62,13 +62,13 @@ describe ApplicationHelper do
avatar_url = "/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
+ .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
allow(ActionController::Base).to receive(:asset_host).and_return(gitlab_host)
avatar_url = "#{gitlab_host}/uploads/-/system/project/avatar/#{project.id}/banana_sample.gif"
expect(helper.project_icon(project.full_path).to_s)
- .to eq "<img src=\"#{avatar_url}\" alt=\"Banana sample\" />"
+ .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
it 'gives uploaded icon when present' do
@@ -77,7 +77,8 @@ describe ApplicationHelper do
allow_any_instance_of(Project).to receive(:avatar_in_git).and_return(true)
avatar_url = "#{gitlab_host}#{project_avatar_path(project)}"
- expect(helper.project_icon(project.full_path).to_s).to match(image_tag(avatar_url))
+ expect(helper.project_icon(project.full_path).to_s)
+ .to eq "<img data-src=\"#{avatar_url}\" class=\" lazy\" src=\"#{LazyImageTagHelper.placeholder_image}\" />"
end
end
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 049475a5408..d16fcf21e45 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -27,11 +27,11 @@ describe AvatarsHelper do
it 'displays user avatar' do
is_expected.to eq image_tag(
- avatar_icon(user, 16),
- class: 'avatar has-tooltip s16 ',
+ LazyImageTagHelper.placeholder_image,
+ class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar",
title: user.name,
- data: { container: 'body' }
+ data: { container: 'body', src: avatar_icon(user, 16) }
)
end
@@ -40,22 +40,8 @@ describe AvatarsHelper do
it 'uses provided css_class' do
is_expected.to eq image_tag(
- avatar_icon(user, 16),
- class: "avatar has-tooltip s16 #{options[:css_class]}",
- alt: "#{user.name}'s avatar",
- title: user.name,
- data: { container: 'body' }
- )
- end
- end
-
- context 'with lazy parameter' do
- let(:options) { { user: user, lazy: true } }
-
- it 'uses data-src instead of src' do
- is_expected.to eq image_tag(
- '',
- class: 'avatar has-tooltip s16 ',
+ LazyImageTagHelper.placeholder_image,
+ class: "avatar has-tooltip s16 #{options[:css_class]} lazy",
alt: "#{user.name}'s avatar",
title: user.name,
data: { container: 'body', src: avatar_icon(user, 16) }
@@ -68,11 +54,11 @@ describe AvatarsHelper do
it 'uses provided size' do
is_expected.to eq image_tag(
- avatar_icon(user, options[:size]),
- class: "avatar has-tooltip s#{options[:size]} ",
+ LazyImageTagHelper.placeholder_image,
+ class: "avatar has-tooltip s#{options[:size]} lazy",
alt: "#{user.name}'s avatar",
title: user.name,
- data: { container: 'body' }
+ data: { container: 'body', src: avatar_icon(user, options[:size]) }
)
end
end
@@ -82,11 +68,11 @@ describe AvatarsHelper do
it 'uses provided url' do
is_expected.to eq image_tag(
- options[:url],
- class: 'avatar has-tooltip s16 ',
+ LazyImageTagHelper.placeholder_image,
+ class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar",
title: user.name,
- data: { container: 'body' }
+ data: { container: 'body', src: options[:url] }
)
end
end
@@ -99,22 +85,22 @@ describe AvatarsHelper do
it 'prefers user parameter' do
is_expected.to eq image_tag(
- avatar_icon(user, 16),
- class: 'avatar has-tooltip s16 ',
+ LazyImageTagHelper.placeholder_image,
+ class: 'avatar has-tooltip s16 lazy',
alt: "#{user.name}'s avatar",
title: user.name,
- data: { container: 'body' }
+ data: { container: 'body', src: avatar_icon(user, 16) }
)
end
end
it 'uses user_name and user_email parameter if user is not present' do
is_expected.to eq image_tag(
- avatar_icon(options[:user_email], 16),
- class: 'avatar has-tooltip s16 ',
+ LazyImageTagHelper.placeholder_image,
+ class: 'avatar has-tooltip s16 lazy',
alt: "#{options[:user_name]}'s avatar",
title: options[:user_name],
- data: { container: 'body' }
+ data: { container: 'body', src: avatar_icon(options[:user_email], 16) }
)
end
end
diff --git a/spec/helpers/issues_helper_spec.rb b/spec/helpers/issues_helper_spec.rb
index 8f7f17a484f..9524a101e74 100644
--- a/spec/helpers/issues_helper_spec.rb
+++ b/spec/helpers/issues_helper_spec.rb
@@ -8,7 +8,7 @@ describe IssuesHelper do
describe "url_for_issue" do
let(:issues_url) { ext_project.external_issue_tracker.issues_url}
let(:ext_expected) { issues_url.gsub(':id', issue.iid.to_s).gsub(':project_id', ext_project.id.to_s) }
- let(:int_expected) { polymorphic_path([@project.namespace, project, issue]) }
+ let(:int_expected) { polymorphic_path([@project.namespace, @project, issue]) }
it "returns internal path if used internal tracker" do
@project = project
@@ -22,6 +22,12 @@ describe IssuesHelper do
expect(url_for_issue(issue.iid)).to match(ext_expected)
end
+ it "returns path to internal issue when internal option passed" do
+ @project = ext_project
+
+ expect(url_for_issue(issue.iid, ext_project, internal: true)).to match(int_expected)
+ end
+
it "returns empty string if project nil" do
@project = nil
diff --git a/spec/javascripts/lazy_loader_spec.js b/spec/javascripts/lazy_loader_spec.js
new file mode 100644
index 00000000000..1d81e4e2d1a
--- /dev/null
+++ b/spec/javascripts/lazy_loader_spec.js
@@ -0,0 +1,57 @@
+import LazyLoader from '~/lazy_loader';
+
+let lazyLoader = null;
+
+describe('LazyLoader', function () {
+ preloadFixtures('issues/issue_with_comment.html.raw');
+
+ beforeEach(function () {
+ loadFixtures('issues/issue_with_comment.html.raw');
+ lazyLoader = new LazyLoader({
+ observerNode: 'body',
+ });
+ // Doing everything that happens normally in onload
+ lazyLoader.loadCheck();
+ });
+ describe('behavior', function () {
+ it('should copy value from data-src to src for img 1', function (done) {
+ const img = document.querySelectorAll('img[data-src]')[0];
+ const originalDataSrc = img.getAttribute('data-src');
+ img.scrollIntoView();
+
+ setTimeout(() => {
+ expect(img.getAttribute('src')).toBe(originalDataSrc);
+ expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
+ done();
+ }, 100);
+ });
+
+ it('should lazy load dynamically added data-src images', function (done) {
+ const newImg = document.createElement('img');
+ const testPath = '/img/testimg.png';
+ newImg.className = 'lazy';
+ newImg.setAttribute('data-src', testPath);
+ document.body.appendChild(newImg);
+ newImg.scrollIntoView();
+
+ setTimeout(() => {
+ expect(newImg.getAttribute('src')).toBe(testPath);
+ expect(document.getElementsByClassName('js-lazy-loaded').length).toBeGreaterThan(0);
+ done();
+ }, 100);
+ });
+
+ it('should not alter normal images', function (done) {
+ const newImg = document.createElement('img');
+ const testPath = '/img/testimg.png';
+ newImg.setAttribute('src', testPath);
+ document.body.appendChild(newImg);
+ newImg.scrollIntoView();
+
+ setTimeout(() => {
+ expect(newImg).not.toHaveClass('js-lazy-loaded');
+ done();
+ }, 100);
+ });
+ });
+});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
index d4b200875df..ab8a3f6c64c 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_deployment_spec.js
@@ -10,6 +10,7 @@ const deploymentMockData = [
url: '/root/acets-review-apps/environments/15',
stop_url: '/root/acets-review-apps/environments/15/stop',
metrics_url: '/root/acets-review-apps/environments/15/deployments/1/metrics',
+ metrics_monitoring_url: '/root/acets-review-apps/environments/15/metrics',
external_url: 'http://diplo.',
external_url_formatted: 'diplo.',
deployed_at: '2017-03-22T22:44:42.258Z',
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
index 2c3d0ddff28..6adcbc73ed7 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_memory_usage_spec.js
@@ -3,6 +3,7 @@ import memoryUsageComponent from '~/vue_merge_request_widget/components/mr_widge
import MRWidgetService from '~/vue_merge_request_widget/services/mr_widget_service';
const url = '/root/acets-review-apps/environments/15/deployments/1/metrics';
+const monitoringUrl = '/root/acets-review-apps/environments/15/metrics';
const metricsMockData = {
success: true,
@@ -39,6 +40,7 @@ const createComponent = () => {
el: document.createElement('div'),
propsData: {
metricsUrl: url,
+ metricsMonitoringUrl: monitoringUrl,
memoryMetrics: [],
deploymentTime: 0,
hasMetrics: false,
diff --git a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
index b7d82c36ddd..fb320e0148a 100644
--- a/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/external_issue_reference_filter_spec.rb
@@ -108,6 +108,11 @@ describe Banzai::Filter::ExternalIssueReferenceFilter, lib: true do
let(:issue) { ExternalIssue.new("#123", project) }
let(:reference) { issue.to_reference }
+ before do
+ project.issues_enabled = false
+ project.save!
+ end
+
it_behaves_like "external issue tracker"
end
diff --git a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
index 082c0d4dd0d..cbb2808c6bb 100644
--- a/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
+++ b/spec/lib/banzai/filter/gollum_tags_filter_spec.rb
@@ -22,7 +22,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
tag = '[[images/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
- expect(doc.at_css('img')['src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
+ expect(doc.at_css('img')['data-src']).to eq "#{project_wiki.wiki_base_path}/images/image.jpg"
end
it 'does not creates img tag if image does not exist' do
@@ -40,7 +40,7 @@ describe Banzai::Filter::GollumTagsFilter, lib: true do
tag = '[[http://example.com/image.jpg]]'
doc = filter("See #{tag}", project_wiki: project_wiki)
- expect(doc.at_css('img')['src']).to eq "http://example.com/image.jpg"
+ expect(doc.at_css('img')['data-src']).to eq "http://example.com/image.jpg"
end
it 'does not creates img tag for invalid URL' do
diff --git a/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
new file mode 100644
index 00000000000..c19de7b784a
--- /dev/null
+++ b/spec/lib/banzai/filter/image_lazy_load_filter_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Banzai::Filter::ImageLazyLoadFilter, lib: true do
+ include FilterSpecHelper
+
+ def image(path)
+ %(<img src="#{path}" />)
+ end
+
+ it 'transforms the image src to a data-src' do
+ doc = filter(image('/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'))
+ expect(doc.at_css('img')['data-src']).to eq '/uploads/e90decf88d8f96fe9e1389afc2e4a91f/test.jpg'
+ end
+
+ it 'works with external images' do
+ doc = filter(image('https://i.imgur.com/DfssX9C.jpg'))
+ expect(doc.at_css('img')['data-src']).to eq 'https://i.imgur.com/DfssX9C.jpg'
+ end
+end
diff --git a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
index 1eb90dc1847..601ffbb5456 100644
--- a/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
+++ b/spec/lib/banzai/pipeline/gfm_pipeline_spec.rb
@@ -4,26 +4,87 @@ describe Banzai::Pipeline::GfmPipeline do
describe 'integration between parsing regular and external issue references' do
let(:project) { create(:redmine_project, :public) }
- it 'allows to use shorthand external reference syntax for Redmine' do
- markdown = '#12'
+ context 'when internal issue tracker is enabled' do
+ context 'when shorthand pattern #ISSUE_ID is used' do
+ it 'links an internal issue if it exists' do
+ issue = create(:issue, project: project)
+ markdown = issue.to_reference(project, full: true)
- result = described_class.call(markdown, project: project)[:output]
- link = result.css('a').first
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
- expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
+ expect(link['href']).to eq(
+ Gitlab::Routing.url_helpers.project_issue_path(project, issue)
+ )
+ end
+
+ it 'does not link any issue if it does not exist on GitLab' do
+ markdown = '#12'
+
+ result = described_class.call(markdown, project: project)[:output]
+ expect(result.css('a')).to be_empty
+ end
+ end
+
+ it 'allows to use long external reference syntax for Redmine' do
+ markdown = 'API_32-12'
+
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
+
+ expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
+ end
+
+ it 'parses cross-project references to regular issues' do
+ other_project = create(:empty_project, :public)
+ issue = create(:issue, project: other_project)
+ markdown = issue.to_reference(project, full: true)
+
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
+
+ expect(link['href']).to eq(
+ Gitlab::Routing.url_helpers.project_issue_path(other_project, issue)
+ )
+ end
end
- it 'parses cross-project references to regular issues' do
- other_project = create(:empty_project, :public)
- issue = create(:issue, project: other_project)
- markdown = issue.to_reference(project, full: true)
+ context 'when internal issue tracker is disabled' do
+ before do
+ project.issues_enabled = false
+ project.save!
+ end
+
+ it 'allows to use shorthand external reference syntax for Redmine' do
+ markdown = '#12'
+
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
+
+ expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
+ end
+
+ it 'allows to use long external reference syntax for Redmine' do
+ markdown = 'API_32-12'
+
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
+
+ expect(link['href']).to eq 'http://redmine/projects/project_name_in_redmine/issues/12'
+ end
+
+ it 'parses cross-project references to regular issues' do
+ other_project = create(:empty_project, :public)
+ issue = create(:issue, project: other_project)
+ markdown = issue.to_reference(project, full: true)
- result = described_class.call(markdown, project: project)[:output]
- link = result.css('a').first
+ result = described_class.call(markdown, project: project)[:output]
+ link = result.css('a').first
- expect(link['href']).to eq(
- Gitlab::Routing.url_helpers.project_issue_path(other_project, issue)
- )
+ expect(link['href']).to eq(
+ Gitlab::Routing.url_helpers.project_issue_path(other_project, issue)
+ )
+ end
end
end
end
diff --git a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
index 114d2490490..5a7a42d84c0 100644
--- a/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/cancelable_spec.rb
@@ -48,7 +48,9 @@ describe Gitlab::Ci::Status::Build::Cancelable do
describe '#has_action?' do
context 'when user is allowed to update build' do
before do
- build.project.team << [user, :developer]
+ stub_not_protect_default_branch
+
+ build.project.add_developer(user)
end
it { is_expected.to have_action }
diff --git a/spec/lib/gitlab/ci/status/build/factory_spec.rb b/spec/lib/gitlab/ci/status/build/factory_spec.rb
index c8a97016f20..8768302eda1 100644
--- a/spec/lib/gitlab/ci/status/build/factory_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/factory_spec.rb
@@ -7,7 +7,9 @@ describe Gitlab::Ci::Status::Build::Factory do
let(:factory) { described_class.new(build, user) }
before do
- project.team << [user, :developer]
+ stub_not_protect_default_branch
+
+ project.add_developer(user)
end
context 'when build is successful' do
@@ -225,19 +227,19 @@ describe Gitlab::Ci::Status::Build::Factory do
end
context 'when user has ability to play action' do
- before do
- project.add_developer(user)
-
- create(:protected_branch, :developers_can_merge,
- name: build.ref, project: project)
- end
-
it 'fabricates status that has action' do
expect(status).to have_action
end
end
context 'when user does not have ability to play action' do
+ before do
+ allow(build.project).to receive(:empty_repo?).and_return(false)
+
+ create(:protected_branch, :no_one_can_push,
+ name: build.ref, project: build.project)
+ end
+
it 'fabricates status that has no action' do
expect(status).not_to have_action
end
@@ -262,6 +264,13 @@ describe Gitlab::Ci::Status::Build::Factory do
end
context 'when user is not allowed to execute manual action' do
+ before do
+ allow(build.project).to receive(:empty_repo?).and_return(false)
+
+ create(:protected_branch, :no_one_can_push,
+ name: build.ref, project: build.project)
+ end
+
it 'fabricates status with correct details' do
expect(status.text).to eq 'manual'
expect(status.group).to eq 'manual'
diff --git a/spec/lib/gitlab/ci/status/build/retryable_spec.rb b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
index 099d873fc01..21026f2c968 100644
--- a/spec/lib/gitlab/ci/status/build/retryable_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/retryable_spec.rb
@@ -48,7 +48,9 @@ describe Gitlab::Ci::Status::Build::Retryable do
describe '#has_action?' do
context 'when user is allowed to update build' do
before do
- build.project.team << [user, :developer]
+ stub_not_protect_default_branch
+
+ build.project.add_developer(user)
end
it { is_expected.to have_action }
diff --git a/spec/lib/gitlab/ci/status/build/stop_spec.rb b/spec/lib/gitlab/ci/status/build/stop_spec.rb
index 23902f26b1a..e0425103f41 100644
--- a/spec/lib/gitlab/ci/status/build/stop_spec.rb
+++ b/spec/lib/gitlab/ci/status/build/stop_spec.rb
@@ -20,7 +20,9 @@ describe Gitlab::Ci::Status::Build::Stop do
describe '#has_action?' do
context 'when user is allowed to update build' do
before do
- build.project.team << [user, :developer]
+ stub_not_protect_default_branch
+
+ build.project.add_developer(user)
end
it { is_expected.to have_action }
diff --git a/spec/lib/gitlab/ci/trace/stream_spec.rb b/spec/lib/gitlab/ci/trace/stream_spec.rb
index 8b925fd4e22..ebe5af56160 100644
--- a/spec/lib/gitlab/ci/trace/stream_spec.rb
+++ b/spec/lib/gitlab/ci/trace/stream_spec.rb
@@ -308,6 +308,20 @@ describe Gitlab::Ci::Trace::Stream do
it { is_expected.to eq('65') }
end
+ context 'long line' do
+ let(:data) { 'a' * 80000 + '100%' + 'a' * 80000 }
+ let(:regex) { '\d+\%' }
+
+ it { is_expected.to eq('100') }
+ end
+
+ context 'many lines' do
+ let(:data) { "foo\n" * 80000 + "100%\n" + "foo\n" * 80000 }
+ let(:regex) { '\d+\%' }
+
+ it { is_expected.to eq('100') }
+ end
+
context 'empty regex' do
let(:data) { 'foo' }
let(:regex) { '' }
diff --git a/spec/lib/gitlab/gitaly_client/repository_service_spec.rb b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
new file mode 100644
index 00000000000..5a9f3fc130c
--- /dev/null
+++ b/spec/lib/gitlab/gitaly_client/repository_service_spec.rb
@@ -0,0 +1,19 @@
+require 'spec_helper'
+
+describe Gitlab::GitalyClient::RepositoryService do
+ set(:project) { create(:empty_project) }
+ let(:storage_name) { project.repository_storage }
+ let(:relative_path) { project.path_with_namespace + '.git' }
+ let(:client) { described_class.new(project.repository) }
+
+ describe '#exists?' do
+ it 'sends an exists message' do
+ expect_any_instance_of(Gitaly::RepositoryService::Stub)
+ .to receive(:exists)
+ .with(gitaly_request_with_path(storage_name, relative_path), kind_of(Hash))
+ .and_call_original
+
+ client.exists?
+ end
+ end
+end
diff --git a/spec/lib/gitlab/path_regex_spec.rb b/spec/lib/gitlab/path_regex_spec.rb
index 1eea710c80b..20be743d224 100644
--- a/spec/lib/gitlab/path_regex_spec.rb
+++ b/spec/lib/gitlab/path_regex_spec.rb
@@ -36,9 +36,12 @@ describe Gitlab::PathRegex, lib: true do
described_class::PROJECT_WILDCARD_ROUTES.include?(path.split('/').first)
end
- def failure_message(missing_words, constant_name, migration_helper)
+ def failure_message(constant_name, migration_helper, missing_words: [], additional_words: [])
missing_words = Array(missing_words)
- <<-MSG
+ additional_words = Array(additional_words)
+ message = ""
+ if missing_words.any?
+ message += <<-MISSING
Found new routes that could cause conflicts with existing namespaced routes
for groups or projects.
@@ -51,7 +54,18 @@ describe Gitlab::PathRegex, lib: true do
Make sure to make a note of the renamed records in the release blog post.
- MSG
+ MISSING
+ end
+
+ if additional_words.any?
+ message += <<-ADDITIONAL
+ Why are <#{additional_words.join(', ')}> in `#{constant_name}`?
+ If they are really required, update these specs to reflect that.
+
+ ADDITIONAL
+ end
+
+ message
end
let(:all_routes) do
@@ -68,9 +82,23 @@ describe Gitlab::PathRegex, lib: true do
let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
let(:top_level_words) do
- routes_not_starting_in_wildcard.map do |route|
+ words = routes_not_starting_in_wildcard.map do |route|
route.split('/')[1]
end.compact.uniq
+
+ words + ee_top_level_words + files_in_public + Array(API::API.prefix.to_s)
+ end
+
+ let(:ee_top_level_words) do
+ ['unsubscribes']
+ end
+
+ let(:files_in_public) do
+ git = Gitlab.config.git.bin_path
+ `cd #{Rails.root} && #{git} ls-files public`
+ .split("\n")
+ .map { |entry| entry.gsub('public/', '') }
+ .uniq
end
# All routes that start with a namespaced path, that have 1 or more
@@ -115,18 +143,29 @@ describe Gitlab::PathRegex, lib: true do
let(:paths_after_group_id) do
group_routes.map do |route|
route.gsub(STARTING_WITH_GROUP, '').split('/').first
- end.uniq
+ end.uniq + ee_paths_after_group_id
+ end
+
+ let(:ee_paths_after_group_id) do
+ %w(analytics
+ ldap
+ ldap_group_links
+ notification_setting
+ audit_events
+ pipeline_quota hooks)
end
describe 'TOP_LEVEL_ROUTES' do
it 'includes all the top level namespaces' do
failure_block = lambda do
missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
- failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
+ additional_words = described_class::TOP_LEVEL_ROUTES - top_level_words
+ failure_message('TOP_LEVEL_ROUTES', 'rename_root_paths',
+ missing_words: missing_words, additional_words: additional_words)
end
expect(described_class::TOP_LEVEL_ROUTES)
- .to include(*top_level_words), failure_block
+ .to contain_exactly(*top_level_words), failure_block
end
end
@@ -134,11 +173,13 @@ describe Gitlab::PathRegex, lib: true do
it "don't contain a second wildcard" do
failure_block = lambda do
missing_words = paths_after_group_id - described_class::GROUP_ROUTES
- failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
+ additional_words = described_class::GROUP_ROUTES - paths_after_group_id
+ failure_message('GROUP_ROUTES', 'rename_child_paths',
+ missing_words: missing_words, additional_words: additional_words)
end
expect(described_class::GROUP_ROUTES)
- .to include(*paths_after_group_id), failure_block
+ .to contain_exactly(*paths_after_group_id), failure_block
end
end
@@ -147,7 +188,7 @@ describe Gitlab::PathRegex, lib: true do
aggregate_failures do
all_wildcard_paths.each do |path|
expect(wildcards_include?(path))
- .to be(true), failure_message(path, 'PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths')
+ .to be(true), failure_message('PROJECT_WILDCARD_ROUTES', 'rename_wildcard_paths', missing_words: path)
end
end
end
diff --git a/spec/lib/gitlab/reference_extractor_spec.rb b/spec/lib/gitlab/reference_extractor_spec.rb
index 84cfd934fa0..917692e9c6c 100644
--- a/spec/lib/gitlab/reference_extractor_spec.rb
+++ b/spec/lib/gitlab/reference_extractor_spec.rb
@@ -183,11 +183,34 @@ describe Gitlab::ReferenceExtractor, lib: true do
context 'with an external issue tracker' do
let(:project) { create(:jira_project) }
+ let(:issue) { create(:issue, project: project) }
+
+ context 'when GitLab issues are enabled' do
+ it 'returns both JIRA and internal issues' do
+ subject.analyze("JIRA-123 and FOOBAR-4567 and #{issue.to_reference}")
+ expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('FOOBAR-4567', project),
+ issue]
+ end
+
+ it 'returns only JIRA issues if the internal one does not exists' do
+ subject.analyze("JIRA-123 and FOOBAR-4567 and #999")
+ expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('FOOBAR-4567', project)]
+ end
+ end
- it 'returns JIRA issues for a JIRA-integrated project' do
- subject.analyze('JIRA-123 and FOOBAR-4567')
- expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
- ExternalIssue.new('FOOBAR-4567', project)]
+ context 'when GitLab issues are disabled' do
+ before do
+ project.issues_enabled = false
+ project.save!
+ end
+
+ it 'returns only JIRA issues' do
+ subject.analyze("JIRA-123 and FOOBAR-4567 and #{issue.to_reference}")
+ expect(subject.issues).to eq [ExternalIssue.new('JIRA-123', project),
+ ExternalIssue.new('FOOBAR-4567', project)]
+ end
end
end
diff --git a/spec/lib/gitlab/untrusted_regexp_spec.rb b/spec/lib/gitlab/untrusted_regexp_spec.rb
index 21d47b7897a..bed58d407ef 100644
--- a/spec/lib/gitlab/untrusted_regexp_spec.rb
+++ b/spec/lib/gitlab/untrusted_regexp_spec.rb
@@ -54,8 +54,8 @@ describe Gitlab::UntrustedRegexp do
let(:regexp) { '' }
let(:text) { 'foo' }
- it 'returns an array of empty matches' do
- is_expected.to eq([''])
+ it 'returns an array of nil matches' do
+ is_expected.to eq([nil, nil, nil, nil])
end
end
@@ -63,8 +63,8 @@ describe Gitlab::UntrustedRegexp do
let(:regexp) { '()' }
let(:text) { 'foo' }
- it 'returns an array of empty matches in an array' do
- is_expected.to eq([['']])
+ it 'returns an array of nil matches in an array' do
+ is_expected.to eq([[nil], [nil], [nil], [nil]])
end
end
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index daf097f8d51..68429d792f2 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -1,11 +1,19 @@
require 'spec_helper'
describe Gitlab::UsageData do
- let!(:project) { create(:empty_project) }
- let!(:project2) { create(:empty_project) }
- let!(:board) { create(:board, project: project) }
+ let(:projects) { create_list(:project, 3) }
+ let!(:board) { create(:board, project: projects[0]) }
describe '#data' do
+ before do
+ create(:jira_service, project: projects[0])
+ create(:jira_service, project: projects[1])
+ create(:prometheus_service, project: projects[1])
+ create(:service, project: projects[0], type: 'SlackSlashCommandsService', active: true)
+ create(:service, project: projects[1], type: 'SlackService', active: true)
+ create(:service, project: projects[2], type: 'SlackService', active: true)
+ end
+
subject { described_class.data }
it "gathers usage data" do
@@ -25,7 +33,7 @@ describe Gitlab::UsageData do
count_data = subject[:counts]
expect(count_data[:boards]).to eq(1)
- expect(count_data[:projects]).to eq(2)
+ expect(count_data[:projects]).to eq(3)
expect(count_data.keys).to match_array(%i(
boards
@@ -49,6 +57,9 @@ describe Gitlab::UsageData do
notes
projects
projects_imported_from_github
+ projects_jira_active
+ projects_slack_notifications_active
+ projects_slack_slash_active
projects_prometheus_active
pages_domains
protected_branches
@@ -59,6 +70,16 @@ describe Gitlab::UsageData do
web_hooks
))
end
+
+ it 'gathers projects data correctly' do
+ count_data = subject[:counts]
+
+ expect(count_data[:projects]).to eq(3)
+ expect(count_data[:projects_prometheus_active]).to eq(1)
+ expect(count_data[:projects_jira_active]).to eq(2)
+ expect(count_data[:projects_slack_notifications_active]).to eq(2)
+ expect(count_data[:projects_slack_slash_active]).to eq(1)
+ end
end
describe '#license_usage_data' do
diff --git a/spec/lib/gitlab/workhorse_spec.rb b/spec/lib/gitlab/workhorse_spec.rb
index 124f66a6e0e..6ca1edb01b9 100644
--- a/spec/lib/gitlab/workhorse_spec.rb
+++ b/spec/lib/gitlab/workhorse_spec.rb
@@ -237,7 +237,8 @@ describe Gitlab::Workhorse, lib: true do
context 'when action is not enabled by feature flag' do
it 'does not include Gitaly params in the returned value' do
- allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag).and_return(false)
+ status_opt_out = Gitlab::GitalyClient::MigrationStatus::OPT_OUT
+ allow(Gitlab::GitalyClient).to receive(:feature_enabled?).with(feature_flag, status: status_opt_out).and_return(false)
expect(subject).not_to include(gitaly_params)
end
@@ -325,7 +326,7 @@ describe Gitlab::Workhorse, lib: true do
subject { described_class.send_git_blob(repository, blob) }
- context 'when Gitaly project_raw_show feature is enabled' do
+ context 'when Gitaly workhorse_raw_show feature is enabled' do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
@@ -345,7 +346,7 @@ describe Gitlab::Workhorse, lib: true do
end
end
- context 'when Gitaly project_raw_show feature is disabled', skip_gitaly_mock: true do
+ context 'when Gitaly workhorse_raw_show feature is disabled', skip_gitaly_mock: true do
it 'sets the header correctly' do
key, command, params = decode_workhorse_header(subject)
diff --git a/spec/models/ability_spec.rb b/spec/models/ability_spec.rb
index dc7a0d80752..58f1a620ab4 100644
--- a/spec/models/ability_spec.rb
+++ b/spec/models/ability_spec.rb
@@ -98,7 +98,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(project).to receive(:owner).twice.and_return(user1)
+ expect(project).to receive(:owner).at_least(:once).and_return(user1)
expect(described_class.users_that_can_read_project(users, project))
.to eq([user1])
@@ -109,7 +109,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(project.team).to receive(:members).twice.and_return([user1])
+ expect(project.team).to receive(:members).at_least(:once).and_return([user1])
expect(described_class.users_that_can_read_project(users, project))
.to eq([user1])
@@ -140,7 +140,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(project).to receive(:owner).twice.and_return(user1)
+ expect(project).to receive(:owner).at_least(:once).and_return(user1)
expect(described_class.users_that_can_read_project(users, project))
.to eq([user1])
@@ -151,7 +151,7 @@ describe Ability, lib: true do
user2 = build(:user, external: true)
users = [user1, user2]
- expect(project.team).to receive(:members).twice.and_return([user1])
+ expect(project.team).to receive(:members).at_least(:once).and_return([user1])
expect(described_class.users_that_can_read_project(users, project))
.to eq([user1])
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index ba0696fa210..bbd45f10b1b 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -734,6 +734,8 @@ describe Ci::Pipeline, models: true do
context 'on failure and build retry' do
before do
+ stub_not_protect_default_branch
+
build.drop
project.add_developer(user)
@@ -999,6 +1001,8 @@ describe Ci::Pipeline, models: true do
let(:latest_status) { pipeline.statuses.latest.pluck(:status) }
before do
+ stub_not_protect_default_branch
+
project.add_developer(user)
end
diff --git a/spec/models/concerns/mentionable_spec.rb b/spec/models/concerns/mentionable_spec.rb
index e2a29e0ae70..1ad811736af 100644
--- a/spec/models/concerns/mentionable_spec.rb
+++ b/spec/models/concerns/mentionable_spec.rb
@@ -174,25 +174,25 @@ describe Commit, 'Mentionable' do
it "is false when message doesn't reference anything" do
allow(commit.raw).to receive(:message).and_return "WIP: Do something"
- expect(commit.matches_cross_reference_regex?).to be false
+ expect(commit.matches_cross_reference_regex?).to be_falsey
end
it 'is true if issue #number mentioned in title' do
allow(commit.raw).to receive(:message).and_return "#1"
- expect(commit.matches_cross_reference_regex?).to be true
+ expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if references an MR' do
allow(commit.raw).to receive(:message).and_return "See merge request !12"
- expect(commit.matches_cross_reference_regex?).to be true
+ expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if references a commit' do
allow(commit.raw).to receive(:message).and_return "a1b2c3d4"
- expect(commit.matches_cross_reference_regex?).to be true
+ expect(commit.matches_cross_reference_regex?).to be_truthy
end
it 'is true if issue referenced by url' do
@@ -200,7 +200,7 @@ describe Commit, 'Mentionable' do
allow(commit.raw).to receive(:message).and_return Gitlab::UrlBuilder.build(issue)
- expect(commit.matches_cross_reference_regex?).to be true
+ expect(commit.matches_cross_reference_regex?).to be_truthy
end
context 'with external issue tracker' do
@@ -209,7 +209,13 @@ describe Commit, 'Mentionable' do
it 'is true if external issues referenced' do
allow(commit.raw).to receive(:message).and_return 'JIRA-123'
- expect(commit.matches_cross_reference_regex?).to be true
+ expect(commit.matches_cross_reference_regex?).to be_truthy
+ end
+
+ it 'is true if internal issues referenced' do
+ allow(commit.raw).to receive(:message).and_return '#123'
+
+ expect(commit.matches_cross_reference_regex?).to be_truthy
end
end
end
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 770176451fe..d8e868265ed 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -236,6 +236,7 @@ describe Group, models: true do
describe '#has_owner?' do
before do
@members = setup_group_members(group)
+ create(:group_member, :invited, :owner, group: group)
end
it { expect(group.has_owner?(@members[:owner])).to be_truthy }
@@ -244,11 +245,13 @@ describe Group, models: true do
it { expect(group.has_owner?(@members[:reporter])).to be_falsey }
it { expect(group.has_owner?(@members[:guest])).to be_falsey }
it { expect(group.has_owner?(@members[:requester])).to be_falsey }
+ it { expect(group.has_owner?(nil)).to be_falsey }
end
describe '#has_master?' do
before do
@members = setup_group_members(group)
+ create(:group_member, :invited, :master, group: group)
end
it { expect(group.has_master?(@members[:owner])).to be_falsey }
@@ -257,6 +260,7 @@ describe Group, models: true do
it { expect(group.has_master?(@members[:reporter])).to be_falsey }
it { expect(group.has_master?(@members[:guest])).to be_falsey }
it { expect(group.has_master?(@members[:requester])).to be_falsey }
+ it { expect(group.has_master?(nil)).to be_falsey }
end
describe '#lfs_enabled?' do
diff --git a/spec/models/list_spec.rb b/spec/models/list_spec.rb
index db2c2619968..a6cc01bea5f 100644
--- a/spec/models/list_spec.rb
+++ b/spec/models/list_spec.rb
@@ -13,12 +13,6 @@ describe List do
it { is_expected.to validate_presence_of(:position) }
it { is_expected.to validate_numericality_of(:position).only_integer.is_greater_than_or_equal_to(0) }
- it 'validates uniqueness of label scoped to board_id' do
- create(:list)
-
- expect(subject).to validate_uniqueness_of(:label_id).scoped_to(:board_id)
- end
-
context 'when list_type is set to closed' do
subject { described_class.new(list_type: :closed) }
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 1eadc28869f..6f6a8ac91b8 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -155,13 +155,53 @@ describe MergeRequest, models: true do
expect { subject.cache_merge_request_closes_issues!(subject.author) }.to change(subject.merge_requests_closing_issues, :count).by(1)
end
- it 'does not cache issues from external trackers' do
- subject.project.update_attribute(:has_external_issue_tracker, true)
- issue = ExternalIssue.new('JIRA-123', subject.project)
- commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
- allow(subject).to receive(:commits).and_return([commit])
+ context 'when both internal and external issue trackers are enabled' do
+ before do
+ subject.project.has_external_issue_tracker = true
+ subject.project.save!
+ end
+
+ it 'does not cache issues from external trackers' do
+ issue = ExternalIssue.new('JIRA-123', subject.project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
- expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
+ end
+
+ it 'caches an internal issue' do
+ issue = create(:issue, project: subject.project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }
+ .to change(subject.merge_requests_closing_issues, :count).by(1)
+ end
+ end
+
+ context 'when only external issue tracker enabled' do
+ before do
+ subject.project.has_external_issue_tracker = true
+ subject.project.issues_enabled = false
+ subject.project.save!
+ end
+
+ it 'does not cache issues from external trackers' do
+ issue = ExternalIssue.new('JIRA-123', subject.project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }.not_to change(subject.merge_requests_closing_issues, :count)
+ end
+
+ it 'does not cache an internal issue' do
+ issue = create(:issue, project: subject.project)
+ commit = double('commit1', safe_message: "Fixes #{issue.to_reference}")
+ allow(subject).to receive(:commits).and_return([commit])
+
+ expect { subject.cache_merge_request_closes_issues!(subject.author) }
+ .not_to change(subject.merge_requests_closing_issues, :count)
+ end
end
end
diff --git a/spec/models/notification_setting_spec.rb b/spec/models/notification_setting_spec.rb
index cc235ad467e..76a7b07949f 100644
--- a/spec/models/notification_setting_spec.rb
+++ b/spec/models/notification_setting_spec.rb
@@ -11,7 +11,14 @@ 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/) }
+
+ describe 'user_id' do
+ before do
+ subject.user = create(:user)
+ end
+
+ it { is_expected.to validate_uniqueness_of(:user_id).scoped_to([:source_type, :source_id]).with_message(/already exists in source/) }
+ end
context "events" do
let(:user) { create(:user) }
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index f9d060d4e0e..d4a777a9bd9 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -11,7 +11,7 @@ describe PagesDomain, models: true do
context 'is unique' do
let(:domain) { 'my.domain.com' }
- it { is_expected.to validate_uniqueness_of(:domain) }
+ it { is_expected.to validate_uniqueness_of(:domain).case_insensitive }
end
{
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 105afed1337..d7d09808a98 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -15,7 +15,6 @@ describe JiraService, models: true do
end
it { is_expected.to validate_presence_of(:url) }
- it { is_expected.to validate_presence_of(:project_key) }
it_behaves_like 'issue tracker service URL attribute', :url
end
@@ -34,7 +33,6 @@ describe JiraService, models: true do
active: true,
username: 'username',
password: 'test',
- project_key: 'TEST',
jira_issue_transition_id: 24,
url: 'http://jira.test.com'
)
@@ -88,7 +86,6 @@ describe JiraService, models: true do
url: 'http://jira.example.com',
username: 'gitlab_jira_username',
password: 'gitlab_jira_password',
- project_key: 'GitLabProject',
jira_issue_transition_id: "custom-id"
)
@@ -196,15 +193,14 @@ describe JiraService, models: true do
project: create(:project),
url: 'http://jira.example.com',
username: 'jira_username',
- password: 'jira_password',
- project_key: 'GitLabProject'
+ password: 'jira_password'
)
end
def test_settings(api_url)
- project_url = "http://#{api_url}/rest/api/2/project/GitLabProject"
+ test_url = "http://#{api_url}/rest/api/2/serverInfo"
- WebMock.stub_request(:get, project_url).with(basic_auth: %w(jira_username jira_password))
+ WebMock.stub_request(:get, test_url).with(basic_auth: %w(jira_username jira_password)).to_return(body: { url: 'http://url' }.to_json )
jira_service.test_settings
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index fdcb011d685..8d916b79b13 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -533,15 +533,48 @@ describe Project, models: true do
end
context 'with external issues tracker' do
+ let!(:internal_issue) { create(:issue, project: project) }
before do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ allow(project).to receive(:external_issue_tracker).and_return(true)
end
- it 'returns an ExternalIssue' do
- issue = project.get_issue('FOO-1234', user)
- expect(issue).to be_kind_of(ExternalIssue)
- expect(issue.iid).to eq 'FOO-1234'
- expect(issue.project).to eq project
+ context 'when internal issues are enabled' do
+ it 'returns interlan issue' do
+ issue = project.get_issue(internal_issue.iid, user)
+
+ expect(issue).to be_kind_of(Issue)
+ expect(issue.iid).to eq(internal_issue.iid)
+ expect(issue.project).to eq(project)
+ end
+
+ it 'returns an ExternalIssue when internal issue does not exists' do
+ issue = project.get_issue('FOO-1234', user)
+
+ expect(issue).to be_kind_of(ExternalIssue)
+ expect(issue.iid).to eq('FOO-1234')
+ expect(issue.project).to eq(project)
+ end
+ end
+
+ context 'when internal issues are disabled' do
+ before do
+ project.issues_enabled = false
+ project.save!
+ end
+
+ it 'returns always an External issues' do
+ issue = project.get_issue(internal_issue.iid, user)
+ expect(issue).to be_kind_of(ExternalIssue)
+ expect(issue.iid).to eq(internal_issue.iid.to_s)
+ expect(issue.project).to eq(project)
+ end
+
+ it 'returns an ExternalIssue when internal issue does not exists' do
+ issue = project.get_issue('FOO-1234', user)
+ expect(issue).to be_kind_of(ExternalIssue)
+ expect(issue.iid).to eq('FOO-1234')
+ expect(issue.project).to eq(project)
+ end
end
end
end
diff --git a/spec/models/project_wiki_spec.rb b/spec/models/project_wiki_spec.rb
index 1f314791479..79ab50c1234 100644
--- a/spec/models/project_wiki_spec.rb
+++ b/spec/models/project_wiki_spec.rb
@@ -21,7 +21,7 @@ describe ProjectWiki, models: true do
describe '#web_url' do
it 'returns the full web URL to the wiki' do
- expect(subject.web_url).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}/wikis/home")
+ expect(subject.web_url).to match("https?://[^\/]+/#{project.path_with_namespace}/wikis/home")
end
end
diff --git a/spec/models/redirect_route_spec.rb b/spec/models/redirect_route_spec.rb
index 71827421dd7..a97af28cb8e 100644
--- a/spec/models/redirect_route_spec.rb
+++ b/spec/models/redirect_route_spec.rb
@@ -11,7 +11,7 @@ describe RedirectRoute, models: true do
describe 'validations' do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
- it { is_expected.to validate_uniqueness_of(:path) }
+ it { is_expected.to validate_uniqueness_of(:path).case_insensitive }
end
describe '.matching_path_and_descendants' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index 7635b0868e7..fcda4248446 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -956,21 +956,25 @@ describe Repository, models: true do
end
end
- describe '#exists?' do
+ shared_examples 'repo exists check' do
it 'returns true when a repository exists' do
expect(repository.exists?).to eq(true)
end
- it 'returns false when a repository does not exist' do
- allow(repository).to receive(:refs_directory_exists?).and_return(false)
+ it 'returns false if no full path can be constructed' do
+ allow(repository).to receive(:path_with_namespace).and_return(nil)
expect(repository.exists?).to eq(false)
end
+ end
- it 'returns false when there is no namespace' do
- allow(repository).to receive(:path_with_namespace).and_return(nil)
+ describe '#exists?' do
+ context 'when repository_exists is disabled' do
+ it_behaves_like 'repo exists check'
+ end
- expect(repository.exists?).to eq(false)
+ context 'when repository_exists is enabled', skip_gitaly_mock: true do
+ it_behaves_like 'repo exists check'
end
end
diff --git a/spec/models/route_spec.rb b/spec/models/route_spec.rb
index 1754253e0f2..12f7611fb28 100644
--- a/spec/models/route_spec.rb
+++ b/spec/models/route_spec.rb
@@ -15,7 +15,7 @@ describe Route, models: true do
it { is_expected.to validate_presence_of(:source) }
it { is_expected.to validate_presence_of(:path) }
- it { is_expected.to validate_uniqueness_of(:path) }
+ it { is_expected.to validate_uniqueness_of(:path).case_insensitive }
end
describe 'callbacks' do
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index a1d6d7e6e0b..20bdb7e37da 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -114,7 +114,9 @@ describe User, models: true do
end
it 'validates uniqueness' do
- expect(subject).to validate_uniqueness_of(:username).case_insensitive
+ user = build(:user)
+
+ expect(user).to validate_uniqueness_of(:username).case_insensitive
end
end
diff --git a/spec/policies/ci/build_policy_spec.rb b/spec/policies/ci/build_policy_spec.rb
index 9f3212b1a63..e3ea3c960a4 100644
--- a/spec/policies/ci/build_policy_spec.rb
+++ b/spec/policies/ci/build_policy_spec.rb
@@ -96,87 +96,57 @@ describe Ci::BuildPolicy, :models do
end
end
- describe 'rules for manual actions' do
+ describe 'rules for protected ref' do
let(:project) { create(:project) }
+ let(:build) { create(:ci_build, ref: 'some-ref', pipeline: pipeline) }
before do
project.add_developer(user)
end
- shared_examples 'protected ref' do
- context 'when build is a manual action' do
- let(:build) do
- create(:ci_build, :manual, ref: 'some-ref', pipeline: pipeline)
- end
-
- it 'does not include ability to update build' do
- expect(policy).to be_disallowed :update_build
- end
+ context 'when no one can push or merge to the branch' do
+ before do
+ create(:protected_branch, :no_one_can_push,
+ name: build.ref, project: project)
end
- context 'when build is not a manual action' do
- let(:build) do
- create(:ci_build, ref: 'some-ref', pipeline: pipeline)
- end
-
- it 'includes ability to update build' do
- expect(policy).to be_allowed :update_build
- end
+ it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :update_build
end
end
- context 'when build is against a protected branch' do
+ context 'when developers can push to the branch' do
before do
- create(:protected_branch, :no_one_can_push,
- name: 'some-ref', project: project)
+ create(:protected_branch, :developers_can_merge,
+ name: build.ref, project: project)
end
- it_behaves_like 'protected ref'
+ it 'includes ability to update build' do
+ expect(policy).to be_allowed :update_build
+ end
end
- context 'when build is against a protected tag' do
+ context 'when no one can create the tag' do
before do
create(:protected_tag, :no_one_can_create,
- name: 'some-ref', project: project)
+ name: build.ref, project: project)
build.update(tag: true)
end
- it_behaves_like 'protected ref'
+ it 'does not include ability to update build' do
+ expect(policy).to be_disallowed :update_build
+ end
end
- context 'when build is against a protected tag but it is not a tag' do
+ context 'when no one can create the tag but it is not a tag' do
before do
create(:protected_tag, :no_one_can_create,
- name: 'some-ref', project: project)
+ name: build.ref, project: project)
end
- context 'when build is a manual action' do
- let(:build) do
- create(:ci_build, :manual, ref: 'some-ref', pipeline: pipeline)
- end
-
- it 'includes ability to update build' do
- expect(policy).to be_allowed :update_build
- end
- end
- end
-
- context 'when branch build is assigned to is not protected' do
- context 'when build is a manual action' do
- let(:build) { create(:ci_build, :manual, pipeline: pipeline) }
-
- it 'includes ability to update build' do
- expect(policy).to be_allowed :update_build
- end
- end
-
- context 'when build is not a manual action' do
- let(:build) { create(:ci_build, pipeline: pipeline) }
-
- it 'includes ability to update build' do
- expect(policy).to be_allowed :update_build
- end
+ it 'includes ability to update build' do
+ expect(policy).to be_allowed :update_build
end
end
end
diff --git a/spec/policies/ci/pipeline_policy_spec.rb b/spec/policies/ci/pipeline_policy_spec.rb
new file mode 100644
index 00000000000..b11b06d301f
--- /dev/null
+++ b/spec/policies/ci/pipeline_policy_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Ci::PipelinePolicy, :models do
+ let(:user) { create(:user) }
+ let(:pipeline) { create(:ci_empty_pipeline, project: project) }
+
+ let(:policy) do
+ described_class.new(user, pipeline)
+ end
+
+ describe 'rules' do
+ describe 'rules for protected ref' do
+ let(:project) { create(:project) }
+
+ before do
+ project.add_developer(user)
+ end
+
+ context 'when no one can push or merge to the branch' do
+ before do
+ create(:protected_branch, :no_one_can_push,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'does not include ability to update pipeline' do
+ expect(policy).to be_disallowed :update_pipeline
+ end
+ end
+
+ context 'when developers can push to the branch' do
+ before do
+ create(:protected_branch, :developers_can_merge,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'includes ability to update pipeline' do
+ expect(policy).to be_allowed :update_pipeline
+ end
+ end
+
+ context 'when no one can create the tag' do
+ before do
+ create(:protected_tag, :no_one_can_create,
+ name: pipeline.ref, project: project)
+
+ pipeline.update(tag: true)
+ end
+
+ it 'does not include ability to update pipeline' do
+ expect(policy).to be_disallowed :update_pipeline
+ end
+ end
+
+ context 'when no one can create the tag but it is not a tag' do
+ before do
+ create(:protected_tag, :no_one_can_create,
+ name: pipeline.ref, project: project)
+ end
+
+ it 'includes ability to update pipeline' do
+ expect(policy).to be_allowed :update_pipeline
+ end
+ end
+ end
+ end
+end
diff --git a/spec/policies/global_policy_spec.rb b/spec/policies/global_policy_spec.rb
index bb0fa0c0e9c..c3e2b603c4b 100644
--- a/spec/policies/global_policy_spec.rb
+++ b/spec/policies/global_policy_spec.rb
@@ -30,5 +30,25 @@ describe GlobalPolicy, models: true do
it { is_expected.to be_allowed(:read_users_list) }
end
end
+
+ context "for an admin" do
+ let(:current_user) { create(:admin) }
+
+ context "when the public level is restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
+ end
+
+ it { is_expected.to be_allowed(:read_users_list) }
+ end
+
+ context "when the public level is not restricted" do
+ before do
+ stub_application_setting(restricted_visibility_levels: [])
+ end
+
+ it { is_expected.to be_allowed(:read_users_list) }
+ end
+ end
end
end
diff --git a/spec/policies/project_policy_spec.rb b/spec/policies/project_policy_spec.rb
index ca435dd0218..f244975e597 100644
--- a/spec/policies/project_policy_spec.rb
+++ b/spec/policies/project_policy_spec.rb
@@ -103,6 +103,48 @@ describe ProjectPolicy, models: true do
end
end
+ context 'issues feature' do
+ subject { described_class.new(owner, project) }
+
+ context 'when the feature is disabled' do
+ it 'does not include the issues permissions' do
+ project.issues_enabled = false
+ project.save!
+
+ expect_disallowed :read_issue, :create_issue, :update_issue, :admin_issue
+ end
+ end
+
+ context 'when the feature is disabled and external tracker configured' do
+ it 'does not include the issues permissions' do
+ create(:jira_service, project: project)
+
+ project.issues_enabled = false
+ project.save!
+
+ expect_disallowed :read_issue, :create_issue, :update_issue, :admin_issue
+ end
+ end
+ end
+
+ context 'when a project has pending invites, and the current user is anonymous' do
+ let(:group) { create(:group, :public) }
+ let(:project) { create(:empty_project, :public, namespace: group) }
+ let(:user_permissions) { [:create_project, :create_issue, :create_note, :upload_file] }
+ let(:anonymous_permissions) { guest_permissions - user_permissions }
+
+ subject { described_class.new(nil, project) }
+
+ before do
+ create(:group_member, :invited, group: group)
+ end
+
+ it 'does not grant owner access' do
+ expect_allowed(*anonymous_permissions)
+ expect_disallowed(*user_permissions)
+ end
+ end
+
context 'abilities for non-public projects' do
let(:project) { create(:empty_project, namespace: owner.namespace) }
diff --git a/spec/requests/api/group_milestones_spec.rb b/spec/requests/api/group_milestones_spec.rb
new file mode 100644
index 00000000000..9b24658771f
--- /dev/null
+++ b/spec/requests/api/group_milestones_spec.rb
@@ -0,0 +1,21 @@
+require 'spec_helper'
+
+describe API::GroupMilestones do
+ let(:user) { create(:user) }
+ let(:group) { create(:group, :private) }
+ let(:project) { create(:empty_project, namespace: group) }
+ let!(:group_member) { create(:group_member, group: group, user: user) }
+ let!(:closed_milestone) { create(:closed_milestone, group: group, title: 'version1', description: 'closed milestone') }
+ let!(:milestone) { create(:milestone, group: group, title: 'version2', description: 'open milestone') }
+
+ it_behaves_like 'group and project milestones', "/groups/:id/milestones" do
+ let(:route) { "/groups/#{group.id}/milestones" }
+ end
+
+ def setup_for_group
+ context_group.update(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
+ context_group.add_developer(user)
+ public_project.update(namespace: context_group)
+ context_group.reload
+ end
+end
diff --git a/spec/requests/api/issues_spec.rb b/spec/requests/api/issues_spec.rb
index 9837fedb522..ff4fc802176 100644
--- a/spec/requests/api/issues_spec.rb
+++ b/spec/requests/api/issues_spec.rb
@@ -693,6 +693,19 @@ describe API::Issues do
expect(json_response['confidential']).to be_falsy
end
+ context 'links exposure' do
+ it 'exposes related resources full URIs' do
+ get api("/projects/#{project.id}/issues/#{issue.iid}", user)
+
+ links = json_response['_links']
+
+ expect(links['self']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}")
+ expect(links['notes']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/notes")
+ expect(links['award_emoji']).to end_with("/api/v4/projects/#{project.id}/issues/#{issue.iid}/award_emoji")
+ expect(links['project']).to end_with("/api/v4/projects/#{project.id}")
+ end
+ end
+
it "returns a project issue by internal id" do
get api("/projects/#{project.id}/issues/#{issue.iid}", user)
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 9098ae6bcda..35b6522ea98 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -794,18 +794,24 @@ describe API::MergeRequests do
it 'handles external issues' do
jira_project = create(:jira_project, :public, name: 'JIR_EXT1')
- issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
- merge_request = create(:merge_request, :simple, author: user, assignee: user, source_project: jira_project)
- merge_request.update_attribute(:description, "Closes #{issue.to_reference(jira_project)}")
+ ext_issue = ExternalIssue.new("#{jira_project.name}-123", jira_project)
+ issue = create(:issue, project: jira_project)
+ description = "Closes #{ext_issue.to_reference(jira_project)}\ncloses #{issue.to_reference}"
+ merge_request = create(:merge_request,
+ :simple, author: user, assignee: user, source_project: jira_project, description: description)
get api("/projects/#{jira_project.id}/merge_requests/#{merge_request.iid}/closes_issues", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.length).to eq(1)
+ expect(json_response.length).to eq(2)
+ expect(json_response.second['title']).to eq(ext_issue.title)
+ expect(json_response.second['id']).to eq(ext_issue.id)
+ expect(json_response.second['confidential']).to be_nil
expect(json_response.first['title']).to eq(issue.title)
expect(json_response.first['id']).to eq(issue.id)
+ expect(json_response.first['confidential']).not_to be_nil
end
it 'returns 403 if the user has no access to the merge request' do
diff --git a/spec/requests/api/project_milestones_spec.rb b/spec/requests/api/project_milestones_spec.rb
new file mode 100644
index 00000000000..fe8fdbfd7e4
--- /dev/null
+++ b/spec/requests/api/project_milestones_spec.rb
@@ -0,0 +1,25 @@
+require 'spec_helper'
+
+describe API::ProjectMilestones do
+ let(:user) { create(:user) }
+ let!(:project) { create(:empty_project, namespace: user.namespace ) }
+ let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
+ let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
+
+ before do
+ project.team << [user, :developer]
+ end
+
+ it_behaves_like 'group and project milestones', "/projects/:id/milestones" do
+ let(:route) { "/projects/#{project.id}/milestones" }
+ end
+
+ describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
+ it 'creates an activity event when an milestone is closed' do
+ expect(Event).to receive(:create)
+
+ put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+ state_event: 'close'
+ end
+ end
+end
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 6dbde8bad31..79e7e1a95df 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -159,6 +159,31 @@ describe API::Projects do
expect(json_response.first).to include 'statistics'
end
+ context 'when external issue tracker is enabled' do
+ let!(:jira_service) { create(:jira_service, project: project) }
+
+ it 'includes open_issues_count' do
+ get api('/projects', user)
+
+ expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first.keys).to include('open_issues_count')
+ expect(json_response.find { |hash| hash['id'] == project.id }.keys).to include('open_issues_count')
+ end
+
+ it 'does not include open_issues_count if issues are disabled' do
+ project.project_feature.update_attribute(:issues_access_level, ProjectFeature::DISABLED)
+
+ get api('/projects', user)
+
+ expect(response.status).to eq 200
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.find { |hash| hash['id'] == project.id }.keys).not_to include('open_issues_count')
+ end
+ end
+
context 'and with simple=true' do
it 'returns a simplified version of all the projects' do
expected_keys = %w(id http_url_to_repo web_url name name_with_namespace path path_with_namespace)
@@ -790,6 +815,38 @@ describe API::Projects do
expect(json_response).not_to include("import_error")
end
+ context 'links exposure' do
+ it 'exposes related resources full URIs' do
+ get api("/projects/#{project.id}", user)
+
+ links = json_response['_links']
+
+ expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
+ expect(links['issues']).to end_with("/api/v4/projects/#{project.id}/issues")
+ expect(links['merge_requests']).to end_with("/api/v4/projects/#{project.id}/merge_requests")
+ expect(links['repo_branches']).to end_with("/api/v4/projects/#{project.id}/repository/branches")
+ expect(links['labels']).to end_with("/api/v4/projects/#{project.id}/labels")
+ expect(links['events']).to end_with("/api/v4/projects/#{project.id}/events")
+ expect(links['members']).to end_with("/api/v4/projects/#{project.id}/members")
+ end
+
+ it 'filters related URIs when their feature is not enabled' do
+ project = create(:empty_project, :public,
+ :merge_requests_disabled,
+ :issues_disabled,
+ creator_id: user.id,
+ namespace: user.namespace)
+
+ get api("/projects/#{project.id}", user)
+
+ links = json_response['_links']
+
+ expect(links.has_key?('merge_requests')).to be_falsy
+ expect(links.has_key?('issues')).to be_falsy
+ expect(links['self']).to end_with("/api/v4/projects/#{project.id}")
+ end
+ end
+
describe 'permissions' do
context 'all projects' do
before do
diff --git a/spec/requests/api/triggers_spec.rb b/spec/requests/api/triggers_spec.rb
index 16ddade27d9..c2636b6614e 100644
--- a/spec/requests/api/triggers_spec.rb
+++ b/spec/requests/api/triggers_spec.rb
@@ -61,7 +61,8 @@ describe API::Triggers do
post api("/projects/#{project.id}/trigger/pipeline"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('No pipeline created')
+ expect(json_response['message']['base'])
+ .to contain_exactly('Reference not found')
end
context 'Validates variables' do
diff --git a/spec/requests/api/users_spec.rb b/spec/requests/api/users_spec.rb
index 877bde3b9a6..66b165b438b 100644
--- a/spec/requests/api/users_spec.rb
+++ b/spec/requests/api/users_spec.rb
@@ -55,17 +55,22 @@ describe API::Users do
context "when public level is restricted" do
before do
stub_application_setting(restricted_visibility_levels: [Gitlab::VisibilityLevel::PUBLIC])
- allow_any_instance_of(API::Helpers).to receive(:authenticate!).and_return(true)
end
- it "renders 403" do
- get api("/users")
- expect(response).to have_http_status(403)
+ context 'when authenticate as a regular user' do
+ it "renders 403" do
+ get api("/users", user)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
end
- it "renders 404" do
- get api("/users/#{user.id}")
- expect(response).to have_http_status(404)
+ context 'when authenticate as an admin' do
+ it "renders 200" do
+ get api("/users", admin)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
end
end
diff --git a/spec/requests/api/v3/triggers_spec.rb b/spec/requests/api/v3/triggers_spec.rb
index d3de6bf13bc..60212660fb6 100644
--- a/spec/requests/api/v3/triggers_spec.rb
+++ b/spec/requests/api/v3/triggers_spec.rb
@@ -52,7 +52,8 @@ describe API::V3::Triggers do
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post v3_api("/projects/#{project.id}/trigger/builds"), options.merge(ref: 'other-branch')
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('No builds created')
+ expect(json_response['message']['base'])
+ .to contain_exactly('Reference not found')
end
context 'Validates variables' do
diff --git a/spec/requests/ci/api/triggers_spec.rb b/spec/requests/ci/api/triggers_spec.rb
index 26b03c0f148..e481ca916ab 100644
--- a/spec/requests/ci/api/triggers_spec.rb
+++ b/spec/requests/ci/api/triggers_spec.rb
@@ -5,7 +5,14 @@ describe Ci::API::Triggers do
let!(:trigger_token) { 'secure token' }
let!(:project) { create(:project, :repository, ci_id: 10) }
let!(:project2) { create(:empty_project, ci_id: 11) }
- let!(:trigger) { create(:ci_trigger, project: project, token: trigger_token) }
+
+ let!(:trigger) do
+ create(:ci_trigger,
+ project: project,
+ token: trigger_token,
+ owner: create(:user))
+ end
+
let(:options) do
{
token: trigger_token
@@ -14,6 +21,8 @@ describe Ci::API::Triggers do
before do
stub_ci_pipeline_to_return_yaml_file
+
+ project.add_developer(trigger.owner)
end
context 'Handles errors' do
@@ -47,7 +56,8 @@ describe Ci::API::Triggers do
it 'returns bad request with no builds created if there\'s no commit for that ref' do
post ci_api("/projects/#{project.ci_id}/refs/other-branch/trigger"), options
expect(response).to have_http_status(400)
- expect(json_response['message']).to eq('No builds created')
+ expect(json_response['message']['base'])
+ .to contain_exactly('Reference not found')
end
context 'Validates variables' do
diff --git a/spec/serializers/build_details_entity_spec.rb b/spec/serializers/build_details_entity_spec.rb
index b92c1c28ba8..1332572fffc 100644
--- a/spec/serializers/build_details_entity_spec.rb
+++ b/spec/serializers/build_details_entity_spec.rb
@@ -9,47 +9,96 @@ describe BuildDetailsEntity do
describe '#as_json' do
let(:project) { create(:project, :repository) }
- let!(:build) { create(:ci_build, :failed, project: project) }
+ let(:pipeline) { create(:ci_pipeline, project: project) }
+ let(:build) { create(:ci_build, :failed, pipeline: pipeline) }
let(:request) { double('request') }
- let(:entity) { described_class.new(build, request: request, current_user: user, project: project) }
+
+ let(:entity) do
+ described_class.new(build, request: request,
+ current_user: user,
+ project: project)
+ end
+
subject { entity.as_json }
before do
allow(request).to receive(:current_user).and_return(user)
end
+ it 'contains the needed key value pairs' do
+ expect(subject).to include(:coverage, :erased_at, :duration)
+ expect(subject).to include(:runner, :pipeline)
+ expect(subject).to include(:raw_path, :new_issue_path)
+ end
+
context 'when the user has access to issues and merge requests' do
- let!(:merge_request) do
- create(:merge_request, source_project: project, source_branch: build.ref)
- end
+ context 'when merge request orginates from the same project' do
+ let(:merge_request) do
+ create(:merge_request, source_project: project, source_branch: build.ref)
+ end
- before do
- allow(build).to receive(:merge_request).and_return(merge_request)
- end
+ before do
+ allow(build).to receive(:merge_request).and_return(merge_request)
+ end
+
+ it 'contains the needed key value pairs' do
+ expect(subject).to include(:merge_request)
+ expect(subject).to include(:new_issue_path)
+ end
- it 'contains the needed key value pairs' do
- expect(subject).to include(:coverage, :erased_at, :duration)
- expect(subject).to include(:runner, :pipeline)
- expect(subject).to include(:raw_path, :merge_request)
- expect(subject).to include(:new_issue_path)
+ it 'exposes correct details of the merge request' do
+ expect(subject[:merge_request][:iid]).to eq merge_request.iid
+ end
+
+ it 'has a correct merge request path' do
+ expect(subject[:merge_request][:path]).to include project.full_path
+ end
end
- it 'exposes details of the merge request' do
- expect(subject[:merge_request]).to include(:iid, :path)
+ context 'when merge request is from a fork' do
+ let(:fork_project) do
+ create(:empty_project, forked_from_project: project)
+ end
+
+ let(:pipeline) { create(:ci_pipeline, project: fork_project) }
+
+ before do
+ allow(build).to receive(:merge_request).and_return(merge_request)
+ end
+
+ let(:merge_request) do
+ create(:merge_request, source_project: fork_project,
+ target_project: project,
+ source_branch: build.ref)
+ end
+
+ it 'contains the needed key value pairs' do
+ expect(subject).to include(:merge_request)
+ expect(subject).to include(:new_issue_path)
+ end
+
+ it 'exposes details of the merge request' do
+ expect(subject[:merge_request][:iid]).to eq merge_request.iid
+ end
+
+ it 'has a merge request path to a target project' do
+ expect(subject[:merge_request][:path])
+ .to include project.full_path
+ end
end
- context 'when the build has been erased' do
- let!(:build) { create(:ci_build, :erasable, project: project) }
+ context 'when the build has not been erased' do
+ let(:build) { create(:ci_build, :erasable, project: project) }
- it 'exposes the user whom erased the build' do
+ it 'exposes a build erase path' do
expect(subject).to include(:erase_path)
end
end
context 'when the build has been erased' do
- let!(:build) { create(:ci_build, erased_at: Time.now, project: project, erased_by: user) }
+ let(:build) { create(:ci_build, :erased, project: project) }
- it 'exposes the user whom erased the build' do
+ it 'exposes the user who erased the build' do
expect(subject).to include(:erased_by)
end
end
diff --git a/spec/serializers/deploy_key_entity_spec.rb b/spec/serializers/deploy_key_entity_spec.rb
index 9620f9665cf..8149de869f1 100644
--- a/spec/serializers/deploy_key_entity_spec.rb
+++ b/spec/serializers/deploy_key_entity_spec.rb
@@ -2,13 +2,15 @@ require 'spec_helper'
describe DeployKeyEntity do
include RequestAwareEntity
-
+
let(:user) { create(:user) }
let(:project) { create(:empty_project, :internal)}
let(:project_private) { create(:empty_project, :private)}
+ let!(:project_pending_delete) { create(:empty_project, :internal, pending_delete: true) }
let(:deploy_key) { create(:deploy_key) }
let!(:deploy_key_internal) { create(:deploy_keys_project, project: project, deploy_key: deploy_key) }
let!(:deploy_key_private) { create(:deploy_keys_project, project: project_private, deploy_key: deploy_key) }
+ let!(:deploy_key_pending_delete) { create(:deploy_keys_project, project: project_pending_delete, deploy_key: deploy_key) }
let(:entity) { described_class.new(deploy_key, user: user) }
diff --git a/spec/serializers/job_entity_spec.rb b/spec/serializers/job_entity_spec.rb
index 5ca7bf2fcaf..026360e91a3 100644
--- a/spec/serializers/job_entity_spec.rb
+++ b/spec/serializers/job_entity_spec.rb
@@ -7,7 +7,9 @@ describe JobEntity do
let(:request) { double('request') }
before do
+ stub_not_protect_default_branch
allow(request).to receive(:current_user).and_return(user)
+
project.add_developer(user)
end
@@ -77,7 +79,7 @@ describe JobEntity do
project.add_developer(user)
create(:protected_branch, :developers_can_merge,
- name: 'master', project: project)
+ name: job.ref, project: job.project)
end
it 'contains path to play action' do
@@ -90,6 +92,13 @@ describe JobEntity do
end
context 'when user is not allowed to trigger action' do
+ before do
+ allow(job.project).to receive(:empty_repo?).and_return(false)
+
+ create(:protected_branch, :no_one_can_push,
+ name: job.ref, project: job.project)
+ end
+
it 'does not contain path to play action' do
expect(subject).not_to include(:play_path)
end
diff --git a/spec/serializers/pipeline_details_entity_spec.rb b/spec/serializers/pipeline_details_entity_spec.rb
index d28dec9592a..b990370a271 100644
--- a/spec/serializers/pipeline_details_entity_spec.rb
+++ b/spec/serializers/pipeline_details_entity_spec.rb
@@ -9,6 +9,8 @@ describe PipelineDetailsEntity do
end
before do
+ stub_not_protect_default_branch
+
allow(request).to receive(:current_user).and_return(user)
end
@@ -52,7 +54,7 @@ describe PipelineDetailsEntity do
context 'user has ability to retry pipeline' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'retryable flag is true' do
@@ -97,7 +99,7 @@ describe PipelineDetailsEntity do
context 'when pipeline has commit statuses' do
let(:pipeline) { create(:ci_empty_pipeline) }
-
+
before do
create(:generic_commit_status, pipeline: pipeline)
end
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 46650f3a80d..5b01cc4fc9e 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -5,6 +5,8 @@ describe PipelineEntity do
let(:request) { double('request') }
before do
+ stub_not_protect_default_branch
+
allow(request).to receive(:current_user).and_return(user)
end
@@ -52,7 +54,7 @@ describe PipelineEntity do
context 'user has ability to retry pipeline' do
before do
- project.team << [user, :developer]
+ project.add_developer(user)
end
it 'contains retry path' do
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index 44813656aff..262bc4acb69 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -108,14 +108,35 @@ describe PipelineSerializer do
end
end
- it 'verifies number of queries', :request_store do
- recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(57)
- expect(recorded.cached_count).to eq(0)
+ shared_examples 'no N+1 queries' do
+ it 'verifies number of queries', :request_store do
+ recorded = ActiveRecord::QueryRecorder.new { subject }
+ expect(recorded.count).to be_within(1).of(59)
+ expect(recorded.cached_count).to eq(0)
+ end
+ end
+
+ context 'with the same ref' do
+ let(:ref) { 'feature' }
+
+ it_behaves_like 'no N+1 queries'
+ end
+
+ context 'with different refs' do
+ def ref
+ @sequence ||= 0
+ @sequence += 1
+ "feature-#{@sequence}"
+ end
+
+ it_behaves_like 'no N+1 queries'
end
def create_pipeline(status)
- create(:ci_empty_pipeline, project: project, status: status).tap do |pipeline|
+ create(:ci_empty_pipeline,
+ project: project,
+ status: status,
+ ref: ref).tap do |pipeline|
Ci::Build::AVAILABLE_STATUSES.each do |status|
create_build(pipeline, status, status)
end
@@ -125,7 +146,7 @@ describe PipelineSerializer do
def create_build(pipeline, stage, status)
create(:ci_build, :tags, :triggered, :artifacts,
pipeline: pipeline, stage: stage,
- name: stage, status: status)
+ name: stage, status: status, ref: pipeline.ref)
end
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index ba07c01d43f..146d25daba3 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -3,19 +3,26 @@ require 'spec_helper'
describe Ci::CreatePipelineService, :services do
let(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
+ let(:ref_name) { 'refs/heads/master' }
before do
stub_ci_pipeline_to_return_yaml_file
end
describe '#execute' do
- def execute_service(source: :push, after: project.commit.id, message: 'Message', ref: 'refs/heads/master')
+ def execute_service(
+ source: :push,
+ after: project.commit.id,
+ message: 'Message',
+ ref: ref_name,
+ trigger_request: nil)
params = { ref: ref,
before: '00000000',
after: after,
commits: [{ message: message }] }
- described_class.new(project, user, params).execute(source)
+ described_class.new(project, user, params).execute(
+ source, trigger_request: trigger_request)
end
context 'valid params' do
@@ -334,5 +341,209 @@ describe Ci::CreatePipelineService, :services do
expect(pipeline.builds.find_by(name: 'rspec').retries_max).to eq 2
end
end
+
+ shared_examples 'when ref is protected' do
+ let(:user) { create(:user) }
+
+ context 'when user is developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'does not create a pipeline' do
+ expect(execute_service).not_to be_persisted
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
+
+ context 'when user is master' do
+ before do
+ project.add_master(user)
+ end
+
+ it 'creates a pipeline' do
+ expect(execute_service).to be_persisted
+ expect(Ci::Pipeline.count).to eq(1)
+ end
+ end
+
+ context 'when trigger belongs to no one' do
+ let(:user) {}
+ let(:trigger_request) { create(:ci_trigger_request) }
+
+ it 'does not create a pipeline' do
+ expect(execute_service(trigger_request: trigger_request))
+ .not_to be_persisted
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
+
+ context 'when trigger belongs to a developer' do
+ let(:user) {}
+
+ let(:trigger_request) do
+ create(:ci_trigger_request).tap do |request|
+ user = create(:user)
+ project.add_developer(user)
+ request.trigger.update(owner: user)
+ end
+ end
+
+ it 'does not create a pipeline' do
+ expect(execute_service(trigger_request: trigger_request))
+ .not_to be_persisted
+ expect(Ci::Pipeline.count).to eq(0)
+ end
+ end
+
+ context 'when trigger belongs to a master' do
+ let(:user) {}
+
+ let(:trigger_request) do
+ create(:ci_trigger_request).tap do |request|
+ user = create(:user)
+ project.add_master(user)
+ request.trigger.update(owner: user)
+ end
+ end
+
+ it 'does not create a pipeline' do
+ expect(execute_service(trigger_request: trigger_request))
+ .to be_persisted
+ expect(Ci::Pipeline.count).to eq(1)
+ end
+ end
+ end
+
+ context 'when ref is a protected branch' do
+ before do
+ create(:protected_branch, project: project, name: 'master')
+ end
+
+ it_behaves_like 'when ref is protected'
+ end
+
+ context 'when ref is a protected tag' do
+ let(:ref_name) { 'refs/tags/v1.0.0' }
+
+ before do
+ create(:protected_tag, project: project, name: '*')
+ end
+
+ it_behaves_like 'when ref is protected'
+ end
+
+ context 'when ref is not protected' do
+ context 'when trigger belongs to no one' do
+ let(:user) {}
+ let(:trigger_request) { create(:ci_trigger_request) }
+
+ it 'creates a pipeline' do
+ expect(execute_service(trigger_request: trigger_request))
+ .to be_persisted
+ expect(Ci::Pipeline.count).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe '#allowed_to_create?' do
+ let(:user) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:ref) { 'master' }
+
+ subject do
+ described_class.new(project, user, ref: ref)
+ .send(:allowed_to_create?, user)
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the branch is protected' do
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: ref)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'when developers are allowed to merge' do
+ let!(:protected_branch) do
+ create(:protected_branch,
+ :developers_can_merge,
+ project: project,
+ name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+
+ context 'when the tag is protected' do
+ let(:ref) { 'v1.0.0' }
+
+ let!(:protected_tag) do
+ create(:protected_tag, project: project, name: ref)
+ end
+
+ it { is_expected.to be_falsey }
+
+ context 'when developers are allowed to create the tag' do
+ let!(:protected_tag) do
+ create(:protected_tag,
+ :developers_can_create,
+ project: project,
+ name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+ end
+ end
+
+ context 'when user is a master' do
+ before do
+ project.add_master(user)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when the branch is protected' do
+ let!(:protected_branch) do
+ create(:protected_branch, project: project, name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when the tag is protected' do
+ let(:ref) { 'v1.0.0' }
+
+ let!(:protected_tag) do
+ create(:protected_tag, project: project, name: ref)
+ end
+
+ it { is_expected.to be_truthy }
+
+ context 'when no one can create the tag' do
+ let!(:protected_tag) do
+ create(:protected_tag,
+ :no_one_can_create,
+ project: project,
+ name: ref)
+ end
+
+ it { is_expected.to be_falsey }
+ end
+ end
+ end
+
+ context 'when owner cannot create pipeline' do
+ it { is_expected.to be_falsey }
+ end
end
end
diff --git a/spec/services/ci/create_trigger_request_service_spec.rb b/spec/services/ci/create_trigger_request_service_spec.rb
index f2956262f4b..37ca9804f56 100644
--- a/spec/services/ci/create_trigger_request_service_spec.rb
+++ b/spec/services/ci/create_trigger_request_service_spec.rb
@@ -1,12 +1,15 @@
require 'spec_helper'
describe Ci::CreateTriggerRequestService, services: true do
- let(:service) { described_class.new }
+ let(:service) { described_class }
let(:project) { create(:project, :repository) }
- let(:trigger) { create(:ci_trigger, project: project) }
+ let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
+ let(:owner) { create(:user) }
before do
stub_ci_pipeline_to_return_yaml_file
+
+ project.add_developer(owner)
end
describe '#execute' do
@@ -14,29 +17,26 @@ describe Ci::CreateTriggerRequestService, services: true do
subject { service.execute(project, trigger, 'master') }
context 'without owner' do
- it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) }
it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
it { expect(subject.pipeline).to be_trigger }
- it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
end
context 'with owner' do
- let(:owner) { create(:user) }
- let(:trigger) { create(:ci_trigger, project: project, owner: owner) }
-
- it { expect(subject).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.trigger_request).to be_kind_of(Ci::TriggerRequest) }
+ it { expect(subject.trigger_request.builds.first).to be_kind_of(Ci::Build) }
+ it { expect(subject.trigger_request.builds.first.user).to eq(owner) }
it { expect(subject.pipeline).to be_kind_of(Ci::Pipeline) }
it { expect(subject.pipeline).to be_trigger }
it { expect(subject.pipeline.user).to eq(owner) }
- it { expect(subject.builds.first).to be_kind_of(Ci::Build) }
- it { expect(subject.builds.first.user).to eq(owner) }
end
end
context 'no commit for ref' do
subject { service.execute(project, trigger, 'other-branch') }
- it { expect(subject).to be_nil }
+ it { expect(subject.pipeline).not_to be_persisted }
end
context 'no builds created' do
@@ -46,7 +46,7 @@ describe Ci::CreateTriggerRequestService, services: true do
stub_ci_pipeline_yaml_file('script: { only: [develop], script: hello World }')
end
- it { expect(subject).to be_nil }
+ it { expect(subject.pipeline).not_to be_persisted }
end
end
end
diff --git a/spec/services/ci/process_pipeline_service_spec.rb b/spec/services/ci/process_pipeline_service_spec.rb
index 0934833a4fa..6346f311696 100644
--- a/spec/services/ci/process_pipeline_service_spec.rb
+++ b/spec/services/ci/process_pipeline_service_spec.rb
@@ -9,6 +9,8 @@ describe Ci::ProcessPipelineService, '#execute', :services do
end
before do
+ stub_not_protect_default_branch
+
project.add_developer(user)
end
diff --git a/spec/services/ci/retry_build_service_spec.rb b/spec/services/ci/retry_build_service_spec.rb
index ef9927c5969..2cf62b54666 100644
--- a/spec/services/ci/retry_build_service_spec.rb
+++ b/spec/services/ci/retry_build_service_spec.rb
@@ -85,6 +85,8 @@ describe Ci::RetryBuildService, :services do
context 'when user has ability to execute build' do
before do
+ stub_not_protect_default_branch
+
project.add_developer(user)
end
@@ -131,6 +133,8 @@ describe Ci::RetryBuildService, :services do
context 'when user has ability to execute build' do
before do
+ stub_not_protect_default_branch
+
project.add_developer(user)
end
diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb
index 3e860203063..7798db3f3b9 100644
--- a/spec/services/ci/retry_pipeline_service_spec.rb
+++ b/spec/services/ci/retry_pipeline_service_spec.rb
@@ -244,13 +244,9 @@ describe Ci::RetryPipelineService, '#execute', :services do
create_build('verify', :canceled, 1)
end
- it 'does not reprocess manual action' do
- service.execute(pipeline)
-
- expect(build('test')).to be_pending
- expect(build('deploy')).to be_failed
- expect(build('verify')).to be_created
- expect(pipeline.reload).to be_running
+ it 'raises an error' do
+ expect { service.execute(pipeline) }
+ .to raise_error Gitlab::Access::AccessDeniedError
end
end
@@ -261,13 +257,9 @@ describe Ci::RetryPipelineService, '#execute', :services do
create_build('verify', :canceled, 2)
end
- it 'does not reprocess manual action' do
- service.execute(pipeline)
-
- expect(build('test')).to be_pending
- expect(build('deploy')).to be_failed
- expect(build('verify')).to be_created
- expect(pipeline.reload).to be_running
+ it 'raises an error' do
+ expect { service.execute(pipeline) }
+ .to raise_error Gitlab::Access::AccessDeniedError
end
end
end
diff --git a/spec/services/create_deployment_service_spec.rb b/spec/services/create_deployment_service_spec.rb
index dfab6ebf372..2794721e157 100644
--- a/spec/services/create_deployment_service_spec.rb
+++ b/spec/services/create_deployment_service_spec.rb
@@ -244,6 +244,8 @@ describe CreateDeploymentService, services: true do
context 'when job is retried' do
it_behaves_like 'creates deployment' do
before do
+ stub_not_protect_default_branch
+
project.add_developer(user)
end
diff --git a/spec/services/git_push_service_spec.rb b/spec/services/git_push_service_spec.rb
index c493c08a7ae..f801506f1b6 100644
--- a/spec/services/git_push_service_spec.rb
+++ b/spec/services/git_push_service_spec.rb
@@ -488,21 +488,57 @@ describe GitPushService, services: true do
end
end
- context "using wrong markdown" do
- let(:message) { "this is some work.\n\ncloses #1" }
+ context "using internal issue reference" do
+ context 'when internal issues are disabled' do
+ before do
+ project.issues_enabled = false
+ project.save!
+ end
+ let(:message) { "this is some work.\n\ncloses #1" }
+
+ it "does not initiates one api call to jira server to close the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
+ end
+
+ it "does not initiates one api call to jira server to comment on the issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
+ body: comment_body
+ ).once
+ end
+ end
- it "does not initiates one api call to jira server to close the issue" do
- execute_service(project, commit_author, @oldrev, @newrev, @ref )
+ context 'when internal issues are enabled' do
+ let(:issue) { create(:issue, project: project) }
+ let(:message) { "this is some work.\n\ncloses JIRA-1 \n\n closes #{issue.to_reference}" }
- expect(WebMock).not_to have_requested(:post, jira_api_transition_url('JIRA-1'))
- end
+ it "initiates one api call to jira server to close the jira issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
- it "does not initiates one api call to jira server to comment on the issue" do
- execute_service(project, commit_author, @oldrev, @newrev, @ref )
+ expect(WebMock).to have_requested(:post, jira_api_transition_url('JIRA-1')).once
+ end
- expect(WebMock).not_to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
- body: comment_body
- ).once
+ it "initiates one api call to jira server to comment on the jira issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+
+ expect(WebMock).to have_requested(:post, jira_api_comment_url('JIRA-1')).with(
+ body: comment_body
+ ).once
+ end
+
+ it "closes the internal issue" do
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+ expect(issue.reload).to be_closed
+ end
+
+ it "adds a note indicating that the issue is now closed" do
+ expect(SystemNoteService).to receive(:change_status)
+ .with(issue, project, commit_author, "closed", closing_commit)
+ execute_service(project, commit_author, @oldrev, @newrev, @ref )
+ end
end
end
end
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index d6f4c694069..da8b60f1337 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -98,13 +98,13 @@ describe Issues::CloseService, services: true do
end
end
- context 'external issue tracker' do
+ context 'internal issues disabled' do
before do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
- described_class.new(project, user).close_issue(issue)
+ project.issues_enabled = false
+ project.save!
end
- it 'closes the issue' do
+ it 'does not close the issue' do
expect(issue).to be_valid
expect(issue).to be_opened
expect(todo.reload).to be_pending
diff --git a/spec/services/issues/duplicate_service_spec.rb b/spec/services/issues/duplicate_service_spec.rb
new file mode 100644
index 00000000000..82daf53b173
--- /dev/null
+++ b/spec/services/issues/duplicate_service_spec.rb
@@ -0,0 +1,80 @@
+require 'spec_helper'
+
+describe Issues::DuplicateService, services: true do
+ let(:user) { create(:user) }
+ let(:canonical_project) { create(:empty_project) }
+ let(:duplicate_project) { create(:empty_project) }
+
+ let(:canonical_issue) { create(:issue, project: canonical_project) }
+ let(:duplicate_issue) { create(:issue, project: duplicate_project) }
+
+ subject { described_class.new(duplicate_project, user, {}) }
+
+ describe '#execute' do
+ context 'when the issues passed are the same' do
+ it 'does nothing' do
+ expect(subject).not_to receive(:close_service)
+ expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
+ expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)
+
+ subject.execute(duplicate_issue, duplicate_issue)
+ end
+ end
+
+ context 'when the user cannot update the duplicate issue' do
+ before do
+ canonical_project.add_reporter(user)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:close_service)
+ expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
+ expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)
+
+ subject.execute(duplicate_issue, canonical_issue)
+ end
+ end
+
+ context 'when the user cannot comment on the canonical issue' do
+ before do
+ duplicate_project.add_reporter(user)
+ end
+
+ it 'does nothing' do
+ expect(subject).not_to receive(:close_service)
+ expect(SystemNoteService).not_to receive(:mark_duplicate_issue)
+ expect(SystemNoteService).not_to receive(:mark_canonical_issue_of_duplicate)
+
+ subject.execute(duplicate_issue, canonical_issue)
+ end
+ end
+
+ context 'when the user can mark the issue as a duplicate' do
+ before do
+ canonical_project.add_reporter(user)
+ duplicate_project.add_reporter(user)
+ end
+
+ it 'closes the duplicate issue' do
+ subject.execute(duplicate_issue, canonical_issue)
+
+ expect(duplicate_issue.reload).to be_closed
+ expect(canonical_issue.reload).to be_open
+ end
+
+ it 'adds a system note to the duplicate issue' do
+ expect(SystemNoteService)
+ .to receive(:mark_duplicate_issue).with(duplicate_issue, duplicate_project, user, canonical_issue)
+
+ subject.execute(duplicate_issue, canonical_issue)
+ end
+
+ it 'adds a system note to the canonical issue' do
+ expect(SystemNoteService)
+ .to receive(:mark_canonical_issue_of_duplicate).with(canonical_issue, canonical_project, user, duplicate_issue)
+
+ subject.execute(duplicate_issue, canonical_issue)
+ end
+ end
+ end
+end
diff --git a/spec/services/issues/update_service_spec.rb b/spec/services/issues/update_service_spec.rb
index d0b991f19ab..064be940a1c 100644
--- a/spec/services/issues/update_service_spec.rb
+++ b/spec/services/issues/update_service_spec.rb
@@ -491,6 +491,27 @@ describe Issues::UpdateService, services: true do
include_examples 'updating mentions', Issues::UpdateService
end
+ context 'duplicate issue' do
+ let(:canonical_issue) { create(:issue, project: project) }
+
+ context 'invalid canonical_issue_id' do
+ it 'does not call the duplicate service' do
+ expect(Issues::DuplicateService).not_to receive(:new)
+
+ update_issue(canonical_issue_id: 123456789)
+ end
+ end
+
+ context 'valid canonical_issue_id' do
+ it 'calls the duplicate service with both issues' do
+ expect_any_instance_of(Issues::DuplicateService)
+ .to receive(:execute).with(issue, canonical_issue)
+
+ update_issue(canonical_issue_id: canonical_issue.id)
+ end
+ end
+ end
+
include_examples 'issuable update service' do
let(:open_issuable) { issue }
let(:closed_issuable) { create(:closed_issue, project: project) }
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index 01ef52396d7..a40d4c877bc 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -207,7 +207,7 @@ describe MergeRequests::BuildService, services: true do
let(:source_branch) { '12345-fix-issue' }
before do
- allow(project).to receive(:default_issues_tracker?).and_return(false)
+ allow(project).to receive(:external_issue_tracker).and_return(true)
end
it 'sets the title to: Resolves External Issue $issue-iid' do
diff --git a/spec/services/projects/update_service_spec.rb b/spec/services/projects/update_service_spec.rb
index fd4011ad606..3ee834748df 100644
--- a/spec/services/projects/update_service_spec.rb
+++ b/spec/services/projects/update_service_spec.rb
@@ -103,7 +103,7 @@ describe Projects::UpdateService, '#execute', :services do
end
end
- context 'when renaming project that contains container images' do
+ context 'when updating a project that contains container images' do
before do
stub_container_registry_config(enabled: true)
stub_container_registry_tags(repository: /image/, tags: %w[rc1])
@@ -116,6 +116,13 @@ describe Projects::UpdateService, '#execute', :services do
expect(result).to include(status: :error)
expect(result[:message]).to match(/contains container registry tags/)
end
+
+ it 'allows to update other settings' do
+ result = update_project(project, admin, public_builds: true)
+
+ expect(result[:status]).to eq :success
+ expect(project.reload.public_builds).to be true
+ end
end
context 'when passing invalid parameters' do
diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb
index a2db3f68ff7..2a2a5c38e4b 100644
--- a/spec/services/quick_actions/interpret_service_spec.rb
+++ b/spec/services/quick_actions/interpret_service_spec.rb
@@ -261,6 +261,15 @@ describe QuickActions::InterpretService, services: true do
end
end
+ shared_examples 'duplicate command' do
+ it 'fetches issue and populates canonical_issue_id if content contains /duplicate issue_reference' do
+ issue_duplicate # populate the issue
+ _, updates = service.execute(content, issuable)
+
+ expect(updates).to eq(canonical_issue_id: issue_duplicate.id)
+ end
+ end
+
it_behaves_like 'reopen command' do
let(:content) { '/reopen' }
let(:issuable) { issue }
@@ -644,6 +653,41 @@ describe QuickActions::InterpretService, services: true do
let(:issuable) { issue }
end
+ context '/duplicate command' do
+ it_behaves_like 'duplicate command' do
+ let(:issue_duplicate) { create(:issue, project: project) }
+ let(:content) { "/duplicate #{issue_duplicate.to_reference}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/duplicate' }
+ let(:issuable) { issue }
+ end
+
+ context 'cross project references' do
+ it_behaves_like 'duplicate command' do
+ let(:other_project) { create(:empty_project, :public) }
+ let(:issue_duplicate) { create(:issue, project: other_project) }
+ let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:content) { "/duplicate imaginary#1234" }
+ let(:issuable) { issue }
+ end
+
+ it_behaves_like 'empty command' do
+ let(:other_project) { create(:empty_project, :private) }
+ let(:issue_duplicate) { create(:issue, project: other_project) }
+
+ let(:content) { "/duplicate #{issue_duplicate.to_reference(project)}" }
+ let(:issuable) { issue }
+ end
+ end
+ end
+
context 'when current_user cannot :admin_issue' do
let(:visitor) { create(:user) }
let(:issue) { create(:issue, project: project, author: visitor) }
@@ -693,6 +737,11 @@ describe QuickActions::InterpretService, services: true do
let(:content) { '/remove_due_date' }
let(:issuable) { issue }
end
+
+ it_behaves_like 'empty command' do
+ let(:content) { '/duplicate #{issue.to_reference}' }
+ let(:issuable) { issue }
+ end
end
context '/award command' do
diff --git a/spec/services/system_note_service_spec.rb b/spec/services/system_note_service_spec.rb
index 60477b8e9ba..681b419aedf 100644
--- a/spec/services/system_note_service_spec.rb
+++ b/spec/services/system_note_service_spec.rb
@@ -1101,4 +1101,54 @@ describe SystemNoteService, services: true do
expect(subject.note).to include(diffs_project_merge_request_url(project, merge_request, diff_id: diff_id, anchor: line_code))
end
end
+
+ describe '.mark_duplicate_issue' do
+ subject { described_class.mark_duplicate_issue(noteable, project, author, canonical_issue) }
+
+ context 'within the same project' do
+ let(:canonical_issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference}" }
+ end
+
+ context 'across different projects' do
+ let(:other_project) { create(:empty_project) }
+ let(:canonical_issue) { create(:issue, project: other_project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked this issue as a duplicate of #{canonical_issue.to_reference(project)}" }
+ end
+ end
+
+ describe '.mark_canonical_issue_of_duplicate' do
+ subject { described_class.mark_canonical_issue_of_duplicate(noteable, project, author, duplicate_issue) }
+
+ context 'within the same project' do
+ let(:duplicate_issue) { create(:issue, project: project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference} as a duplicate of this issue" }
+ end
+
+ context 'across different projects' do
+ let(:other_project) { create(:empty_project) }
+ let(:duplicate_issue) { create(:issue, project: other_project) }
+
+ it_behaves_like 'a system note' do
+ let(:action) { 'duplicate' }
+ end
+
+ it { expect(subject.note).to eq "marked #{duplicate_issue.to_reference(project)} as a duplicate of this issue" }
+ end
+ end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 5d5715b10ff..e7329210896 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -148,3 +148,10 @@ FactoryGirl::SyntaxRunner.class_eval do
end
ActiveRecord::Migration.maintain_test_schema!
+
+Shoulda::Matchers.configure do |config|
+ config.integrate do |with|
+ with.test_framework :rspec
+ with.library :rails
+ end
+end
diff --git a/spec/requests/api/milestones_spec.rb b/spec/support/api/milestones_shared_examples.rb
index ab5ea3e8f2c..480e7d5151f 100644
--- a/spec/requests/api/milestones_spec.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -1,21 +1,14 @@
-require 'spec_helper'
-
-describe API::Milestones do
- let(:user) { create(:user) }
- let!(:project) { create(:empty_project, namespace: user.namespace ) }
- let!(:closed_milestone) { create(:closed_milestone, project: project, title: 'version1', description: 'closed milestone') }
- let!(:milestone) { create(:milestone, project: project, title: 'version2', description: 'open milestone') }
+shared_examples_for 'group and project milestones' do |route_definition|
+ let(:resource_route) { "#{route}/#{milestone.id}" }
let(:label_1) { create(:label, title: 'label_1', project: project, priority: 1) }
let(:label_2) { create(:label, title: 'label_2', project: project, priority: 2) }
let(:label_3) { create(:label, title: 'label_3', project: project) }
+ let(:merge_request) { create(:merge_request, source_project: project) }
+ let(:another_merge_request) { create(:merge_request, :simple, source_project: project) }
- before do
- project.team << [user, :developer]
- end
-
- describe 'GET /projects/:id/milestones' do
- it 'returns project milestones' do
- get api("/projects/#{project.id}/milestones", user)
+ describe "GET #{route_definition}" do
+ it 'returns milestones list' do
+ get api(route, user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -24,13 +17,13 @@ describe API::Milestones do
end
it 'returns a 401 error if user not authenticated' do
- get api("/projects/#{project.id}/milestones")
+ get api(route)
expect(response).to have_http_status(401)
end
it 'returns an array of active milestones' do
- get api("/projects/#{project.id}/milestones?state=active", user)
+ get api("#{route}/?state=active", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -40,7 +33,7 @@ describe API::Milestones do
end
it 'returns an array of closed milestones' do
- get api("/projects/#{project.id}/milestones?state=closed", user)
+ get api("#{route}/?state=closed", user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -50,9 +43,9 @@ describe API::Milestones do
end
it 'returns an array of milestones specified by iids' do
- other_milestone = create(:milestone, project: project)
+ other_milestone = create(:milestone, project: try(:project), group: try(:group))
- get api("/projects/#{project.id}/milestones", user), iids: [closed_milestone.iid, other_milestone.iid]
+ get api(route, user), iids: [closed_milestone.iid, other_milestone.iid]
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -61,25 +54,15 @@ describe API::Milestones do
end
it 'does not return any milestone if none found' do
- get api("/projects/#{project.id}/milestones", user), iids: [Milestone.maximum(:iid).succ]
+ get api(route, user), iids: [Milestone.maximum(:iid).succ]
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
expect(json_response.length).to eq(0)
end
- end
-
- describe 'GET /projects/:id/milestones/:milestone_id' do
- it 'returns a project milestone by id' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
-
- expect(response).to have_http_status(200)
- expect(json_response['title']).to eq(milestone.title)
- expect(json_response['iid']).to eq(milestone.iid)
- end
- it 'returns a project milestone by iids array' do
- get api("/projects/#{project.id}/milestones?iids=#{closed_milestone.iid}", user)
+ it 'returns a milestone by iids array' do
+ get api("#{route}?iids=#{closed_milestone.iid}", user)
expect(response.status).to eq 200
expect(response).to include_pagination_headers
@@ -89,8 +72,8 @@ describe API::Milestones do
expect(json_response.first['id']).to eq closed_milestone.id
end
- it 'returns a project milestone by searching for title' do
- get api("/projects/#{project.id}/milestones", user), search: 'version2'
+ it 'returns a milestone by searching for title' do
+ get api(route, user), search: 'version2'
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -99,8 +82,8 @@ describe API::Milestones do
expect(json_response.first['id']).to eq milestone.id
end
- it 'returns a project milestones by searching for description' do
- get api("/projects/#{project.id}/milestones", user), search: 'open'
+ it 'returns a milestones by searching for description' do
+ get api(route, user), search: 'open'
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -110,9 +93,17 @@ describe API::Milestones do
end
end
- describe 'GET /projects/:id/milestones/:milestone_id' do
- it 'returns a project milestone by id' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}", user)
+ describe "GET #{route_definition}/:milestone_id" do
+ it 'returns a milestone by id' do
+ get api(resource_route, user)
+
+ expect(response).to have_http_status(200)
+ expect(json_response['title']).to eq(milestone.title)
+ expect(json_response['iid']).to eq(milestone.iid)
+ end
+
+ it 'returns a milestone by id' do
+ get api(resource_route, user)
expect(response).to have_http_status(200)
expect(json_response['title']).to eq(milestone.title)
@@ -120,29 +111,29 @@ describe API::Milestones do
end
it 'returns 401 error if user not authenticated' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}")
+ get api(resource_route)
expect(response).to have_http_status(401)
end
it 'returns a 404 error if milestone id not found' do
- get api("/projects/#{project.id}/milestones/1234", user)
+ get api("#{route}/1234", user)
expect(response).to have_http_status(404)
end
end
- describe 'POST /projects/:id/milestones' do
- it 'creates a new project milestone' do
- post api("/projects/#{project.id}/milestones", user), title: 'new milestone'
+ describe "POST #{route_definition}" do
+ it 'creates a new milestone' do
+ post api(route, user), title: 'new milestone'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('new milestone')
expect(json_response['description']).to be_nil
end
- it 'creates a new project milestone with description and dates' do
- post api("/projects/#{project.id}/milestones", user),
+ it 'creates a new milestone with description and dates' do
+ post api(route, user),
title: 'new milestone', description: 'release', due_date: '2013-03-02', start_date: '2013-02-02'
expect(response).to have_http_status(201)
@@ -152,20 +143,20 @@ describe API::Milestones do
end
it 'returns a 400 error if title is missing' do
- post api("/projects/#{project.id}/milestones", user)
+ post api(route, user)
expect(response).to have_http_status(400)
end
it 'returns a 400 error if params are invalid (duplicate title)' do
- post api("/projects/#{project.id}/milestones", user),
+ post api(route, user),
title: milestone.title, description: 'release', due_date: '2013-03-02'
expect(response).to have_http_status(400)
end
- it 'creates a new project with reserved html characters' do
- post api("/projects/#{project.id}/milestones", user), title: 'foo & bar 1.1 -> 2.2'
+ it 'creates a new milestone with reserved html characters' do
+ post api(route, user), title: 'foo & bar 1.1 -> 2.2'
expect(response).to have_http_status(201)
expect(json_response['title']).to eq('foo & bar 1.1 -> 2.2')
@@ -173,9 +164,9 @@ describe API::Milestones do
end
end
- describe 'PUT /projects/:id/milestones/:milestone_id' do
- it 'updates a project milestone' do
- put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+ describe "PUT #{route_definition}/:milestone_id" do
+ it 'updates a milestone' do
+ put api(resource_route, user),
title: 'updated title'
expect(response).to have_http_status(200)
@@ -185,23 +176,21 @@ describe API::Milestones do
it 'removes a due date if nil is passed' do
milestone.update!(due_date: "2016-08-05")
- put api("/projects/#{project.id}/milestones/#{milestone.id}", user), due_date: nil
+ put api(resource_route, user), due_date: nil
expect(response).to have_http_status(200)
expect(json_response['due_date']).to be_nil
end
it 'returns a 404 error if milestone id not found' do
- put api("/projects/#{project.id}/milestones/1234", user),
+ put api("#{route}/1234", user),
title: 'updated title'
expect(response).to have_http_status(404)
end
- end
- describe 'PUT /projects/:id/milestones/:milestone_id to close milestone' do
- it 'updates a project milestone' do
- put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
+ it 'closes milestone' do
+ put api(resource_route, user),
state_event: 'close'
expect(response).to have_http_status(200)
@@ -209,21 +198,14 @@ describe API::Milestones do
end
end
- describe 'PUT /projects/:id/milestones/:milestone_id to test observer on close' do
- it 'creates an activity event when an milestone is closed' do
- expect(Event).to receive(:create)
-
- put api("/projects/#{project.id}/milestones/#{milestone.id}", user),
- state_event: 'close'
- end
- end
+ describe "GET #{route_definition}/:milestone_id/issues" do
+ let(:issues_route) { "#{route}/#{milestone.id}/issues" }
- describe 'GET /projects/:id/milestones/:milestone_id/issues' do
before do
milestone.issues << create(:issue, project: project)
end
- it 'returns project issues for a particular milestone' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+ it 'returns issues for a particular milestone' do
+ get api(issues_route, user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -231,12 +213,12 @@ describe API::Milestones do
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
- it 'returns project issues sorted by label priority' do
+ it 'returns issues sorted by label priority' do
issue_1 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_3])
issue_2 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_1])
issue_3 = create(:labeled_issue, project: project, milestone: milestone, labels: [label_2])
- get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+ get api(issues_route, user)
expect(json_response.first['id']).to eq(issue_2.id)
expect(json_response.second['id']).to eq(issue_3.id)
@@ -244,44 +226,58 @@ describe API::Milestones do
end
it 'matches V4 response schema for a list of issues' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}/issues", user)
+ get api(issues_route, user)
expect(response).to have_http_status(200)
expect(response).to match_response_schema('public_api/v4/issues')
end
it 'returns a 401 error if user not authenticated' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}/issues")
+ get api(issues_route)
expect(response).to have_http_status(401)
end
describe 'confidential issues' do
- let(:public_project) { create(:empty_project, :public) }
- let(:milestone) { create(:milestone, project: public_project) }
- let(:issue) { create(:issue, project: public_project) }
- let(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+ let!(:public_project) { create(:empty_project, :public) }
+ let!(:context_group) { try(:group) }
+ let!(:milestone) do
+ context_group ? create(:milestone, group: context_group) : create(:milestone, project: public_project)
+ end
+ let!(:issue) { create(:issue, project: public_project) }
+ let!(:confidential_issue) { create(:issue, confidential: true, project: public_project) }
+ let!(:issues_route) do
+ if context_group
+ "#{route}/#{milestone.id}/issues"
+ else
+ "/projects/#{public_project.id}/milestones/#{milestone.id}/issues"
+ end
+ end
before do
+ # Add public project to the group in context
+ setup_for_group if context_group
+
public_project.team << [user, :developer]
milestone.issues << issue << confidential_issue
end
it 'returns confidential issues to team members' do
- get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+ get api(issues_route, user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.size).to eq(2)
+ # 2 for projects, 3 for group(which has another project with an issue)
+ expect(json_response.size).to be_between(2, 3)
expect(json_response.map { |issue| issue['id'] }).to include(issue.id, confidential_issue.id)
end
it 'does not return confidential issues to team members with guest role' do
member = create(:user)
- project.team << [member, :guest]
+ public_project.team << [member, :guest]
- get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", member)
+ get api(issues_route, member)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -291,7 +287,7 @@ describe API::Milestones do
end
it 'does not return confidential issues to regular users' do
- get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", create(:user))
+ get api(issues_route, create(:user))
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
@@ -304,30 +300,30 @@ describe API::Milestones do
issue.labels << label_2
confidential_issue.labels << label_1
- get api("/projects/#{public_project.id}/milestones/#{milestone.id}/issues", user)
+ get api(issues_route, user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
- expect(json_response.size).to eq(2)
+ # 2 for projects, 3 for group(which has another project with an issue)
+ expect(json_response.size).to be_between(2, 3)
expect(json_response.first['id']).to eq(confidential_issue.id)
expect(json_response.second['id']).to eq(issue.id)
end
end
end
- describe 'GET /projects/:id/milestones/:milestone_id/merge_requests' do
- let(:merge_request) { create(:merge_request, source_project: project) }
- let(:another_merge_request) { create(:merge_request, :simple, source_project: project) }
+ describe "GET #{route_definition}/:milestone_id/merge_requests" do
+ let(:merge_requests_route) { "#{route}/#{milestone.id}/merge_requests" }
before do
milestone.merge_requests << merge_request
end
- it 'returns project merge_requests for a particular milestone' do
+ it 'returns merge_requests for a particular milestone' do
# eager-load another_merge_request
another_merge_request
- get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+ get api(merge_requests_route, user)
expect(response).to have_http_status(200)
expect(json_response).to be_an Array
@@ -336,12 +332,12 @@ describe API::Milestones do
expect(json_response.first['milestone']['title']).to eq(milestone.title)
end
- it 'returns project merge_requests sorted by label priority' do
+ it 'returns merge_requests sorted by label priority' do
merge_request_1 = create(:labeled_merge_request, source_branch: 'branch_1', source_project: project, milestone: milestone, labels: [label_2])
merge_request_2 = create(:labeled_merge_request, source_branch: 'branch_2', source_project: project, milestone: milestone, labels: [label_1])
merge_request_3 = create(:labeled_merge_request, source_branch: 'branch_3', source_project: project, milestone: milestone, labels: [label_3])
- get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+ get api(merge_requests_route, user)
expect(json_response.first['id']).to eq(merge_request_2.id)
expect(json_response.second['id']).to eq(merge_request_1.id)
@@ -349,20 +345,22 @@ describe API::Milestones do
end
it 'returns a 404 error if milestone id not found' do
- get api("/projects/#{project.id}/milestones/1234/merge_requests", user)
+ not_found_route = "#{route}/1234/merge_requests"
+
+ get api(not_found_route, user)
expect(response).to have_http_status(404)
end
it 'returns a 404 if the user has no access to the milestone' do
new_user = create :user
- get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", new_user)
+ get api(merge_requests_route, new_user)
expect(response).to have_http_status(404)
end
it 'returns a 401 error if user not authenticated' do
- get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests")
+ get api(merge_requests_route)
expect(response).to have_http_status(401)
end
@@ -372,7 +370,7 @@ describe API::Milestones do
another_merge_request.labels << label_1
merge_request.labels << label_2
- get api("/projects/#{project.id}/milestones/#{milestone.id}/merge_requests", user)
+ get api(merge_requests_route, user)
expect(response).to have_http_status(200)
expect(response).to include_pagination_headers
diff --git a/spec/support/features/issuable_slash_commands_shared_examples.rb b/spec/support/features/issuable_slash_commands_shared_examples.rb
index 033e338fe61..035428a7d9b 100644
--- a/spec/support/features/issuable_slash_commands_shared_examples.rb
+++ b/spec/support/features/issuable_slash_commands_shared_examples.rb
@@ -5,8 +5,6 @@ shared_examples 'issuable record that supports quick actions in its description
include QuickActionsHelpers
let(:master) { create(:user) }
- let(:assignee) { create(:user, username: 'bob') }
- let(:guest) { create(:user) }
let(:project) { create(:project, :public) }
let!(:milestone) { create(:milestone, project: project, title: 'ASAP') }
let!(:label_bug) { create(:label, project: project, title: 'bug') }
@@ -15,8 +13,6 @@ shared_examples 'issuable record that supports quick actions in its description
before do
project.team << [master, :master]
- project.team << [assignee, :developer]
- project.team << [guest, :guest]
sign_in(master)
end
@@ -57,6 +53,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing commands' do
it 'creates a note without the commands and interpret the commands accordingly' do
+ assignee = create(:user, username: 'bob')
write_note("Awesome!\n/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).to have_content 'Awesome!'
@@ -77,6 +74,7 @@ shared_examples 'issuable record that supports quick actions in its description
context 'with a note containing only commands' do
it 'does not create a note but interpret the commands accordingly' do
+ assignee = create(:user, username: 'bob')
write_note("/assign @bob\n/label ~bug\n/milestone %\"ASAP\"")
expect(page).not_to have_content '/assign @bob'
@@ -111,8 +109,12 @@ shared_examples 'issuable record that supports quick actions in its description
context "when current user cannot close #{issuable_type}" do
before do
+ guest = create(:user)
+ project.add_guest(guest)
+
sign_out(:user)
sign_in(guest)
+
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
@@ -146,8 +148,12 @@ shared_examples 'issuable record that supports quick actions in its description
context "when current user cannot reopen #{issuable_type}" do
before do
+ guest = create(:user)
+ project.add_guest(guest)
+
sign_out(:user)
sign_in(guest)
+
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
end
@@ -176,6 +182,9 @@ shared_examples 'issuable record that supports quick actions in its description
context "when current user cannot change title of #{issuable_type}" do
before do
+ guest = create(:user)
+ project.add_guest(guest)
+
sign_out(:user)
sign_in(guest)
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
@@ -267,6 +276,8 @@ shared_examples 'issuable record that supports quick actions in its description
describe "preview of note on #{issuable_type}" do
it 'removes quick actions from note and explains them' do
+ create(:user, username: 'bob')
+
visit public_send("namespace_project_#{issuable_type}_path", project.namespace, project, issuable)
page.within('.js-main-target-form') do
diff --git a/spec/support/jira_service_helper.rb b/spec/support/jira_service_helper.rb
index 97ae0b6afc5..0b5f66597fd 100644
--- a/spec/support/jira_service_helper.rb
+++ b/spec/support/jira_service_helper.rb
@@ -51,7 +51,7 @@ module JiraServiceHelper
end
def jira_project_url
- JIRA_API + "/project/#{jira_tracker.project_key}"
+ JIRA_API + "/project"
end
def jira_api_comment_url(issue_id)
diff --git a/spec/support/matchers/markdown_matchers.rb b/spec/support/matchers/markdown_matchers.rb
index bbbbaf4c5e8..7afa57fb76b 100644
--- a/spec/support/matchers/markdown_matchers.rb
+++ b/spec/support/matchers/markdown_matchers.rb
@@ -17,7 +17,7 @@ module MarkdownMatchers
image = actual.at_css('img[alt="Relative Image"]')
expect(link['href']).to end_with('master/doc/README.md')
- expect(image['src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
+ expect(image['data-src']).to end_with('master/app/assets/images/touch-icon-ipad.png')
end
end
@@ -70,7 +70,7 @@ module MarkdownMatchers
# GollumTagsFilter
matcher :parse_gollum_tags do
def have_image(src)
- have_css("img[src$='#{src}']")
+ have_css("img[data-src$='#{src}']")
end
prefix = '/namespace1/gitlabhq/wikis'
diff --git a/spec/support/stub_configuration.rb b/spec/support/stub_configuration.rb
index 48f454c7187..80ecce92dc1 100644
--- a/spec/support/stub_configuration.rb
+++ b/spec/support/stub_configuration.rb
@@ -9,6 +9,11 @@ module StubConfiguration
.to receive_messages(messages)
end
+ def stub_not_protect_default_branch
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
def stub_config_setting(messages)
allow(Gitlab.config.gitlab).to receive_messages(messages)
end
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index a8f4bb72acf..74a9f90195c 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -74,6 +74,7 @@ describe PostReceive do
OpenStruct.new(id: '123456')
end
allow_any_instance_of(Ci::CreatePipelineService).to receive(:branch?).and_return(true)
+ allow_any_instance_of(Repository).to receive(:ref_exists?).and_return(true)
stub_ci_pipeline_to_return_yaml_file
end