Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml42
-rw-r--r--.rubocop.yml2
-rw-r--r--CHANGELOG.md40
-rw-r--r--GITLAB_PAGES_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_bundle.js1
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/dispatcher.js11
-rw-r--r--app/assets/javascripts/environments/components/environment.vue15
-rw-r--r--app/assets/javascripts/environments/components/environment_item.vue11
-rw-r--r--app/assets/javascripts/lib/utils/number_utils.js2
-rw-r--r--app/assets/javascripts/merge_request_tabs.js1
-rw-r--r--app/assets/javascripts/monitoring/components/graph.vue14
-rw-r--r--app/assets/javascripts/monitoring/components/graph/legend.vue9
-rw-r--r--app/assets/javascripts/monitoring/components/graph_path.vue (renamed from app/assets/javascripts/monitoring/components/monitoring_paths.vue)0
-rw-r--r--app/assets/javascripts/monitoring/utils/multiple_time_series.js88
-rw-r--r--app/assets/javascripts/notes.js11
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue63
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines.vue7
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table.vue5
-rw-r--r--app/assets/javascripts/pipelines/components/pipelines_table_row.vue9
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_setting.vue104
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue51
-rw-r--r--app/assets/javascripts/projects/permissions/components/project_setting_row.vue36
-rw-r--r--app/assets/javascripts/projects/permissions/components/settings_panel.vue312
-rw-r--r--app/assets/javascripts/projects/permissions/constants.js11
-rw-r--r--app/assets/javascripts/projects/permissions/external.js18
-rw-r--r--app/assets/javascripts/projects/permissions/index.js13
-rw-r--r--app/assets/javascripts/projects_dropdown/components/projects_list_search.vue2
-rw-r--r--app/assets/javascripts/projects_dropdown/components/search.vue2
-rw-r--r--app/assets/javascripts/settings_panels.js4
-rw-r--r--app/assets/javascripts/user_callout.js12
-rw-r--r--app/assets/javascripts/users_select.js12
-rw-r--r--app/assets/javascripts/vue_shared/directives/popover.js20
-rw-r--r--app/assets/stylesheets/framework.scss1
-rw-r--r--app/assets/stylesheets/framework/dropdowns.scss36
-rw-r--r--app/assets/stylesheets/framework/gitlab-theme.scss265
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/assets/stylesheets/framework/nav.scss23
-rw-r--r--app/assets/stylesheets/framework/variables.scss44
-rw-r--r--app/assets/stylesheets/new_nav.scss119
-rw-r--r--app/assets/stylesheets/new_sidebar.scss7
-rw-r--r--app/assets/stylesheets/pages/commits.scss8
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss9
-rw-r--r--app/assets/stylesheets/pages/profiles/preferences.scss64
-rw-r--r--app/assets/stylesheets/pages/projects.scss191
-rw-r--r--app/assets/stylesheets/pages/repo.scss6
-rw-r--r--app/controllers/admin/broadcast_messages_controller.rb2
-rw-r--r--app/controllers/admin/dashboard_controller.rb6
-rw-r--r--app/controllers/admin/users_controller.rb3
-rw-r--r--app/controllers/dashboard/groups_controller.rb4
-rw-r--r--app/controllers/groups_controller.rb22
-rw-r--r--app/controllers/profiles/emails_controller.rb2
-rw-r--r--app/controllers/profiles/keys_controller.rb2
-rw-r--r--app/controllers/profiles/preferences_controller.rb3
-rw-r--r--app/controllers/projects/merge_requests/diffs_controller.rb2
-rw-r--r--app/controllers/projects/pipelines_settings_controller.rb12
-rw-r--r--app/controllers/projects/project_members_controller.rb4
-rw-r--r--app/controllers/projects/settings/ci_cd_controller.rb5
-rw-r--r--app/finders/move_to_project_finder.rb1
-rw-r--r--app/finders/projects_finder.rb2
-rw-r--r--app/finders/todos_finder.rb2
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/auto_devops_helper.rb7
-rw-r--r--app/helpers/commits_helper.rb6
-rw-r--r--app/helpers/groups_helper.rb59
-rw-r--r--app/helpers/issues_helper.rb2
-rw-r--r--app/helpers/preferences_helper.rb4
-rw-r--r--app/helpers/projects_helper.rb47
-rw-r--r--app/helpers/search_helper.rb4
-rw-r--r--app/models/broadcast_message.rb2
-rw-r--r--app/models/ci/build.rb1
-rw-r--r--app/models/ci/pipeline.rb48
-rw-r--r--app/models/concerns/sortable.rb4
-rw-r--r--app/models/environment.rb13
-rw-r--r--app/models/namespace.rb14
-rw-r--r--app/models/project.rb25
-rw-r--r--app/models/project_auto_devops.rb11
-rw-r--r--app/models/user.rb11
-rw-r--r--app/policies/group_policy.rb9
-rw-r--r--app/serializers/environment_entity.rb4
-rw-r--r--app/serializers/environment_serializer.rb6
-rw-r--r--app/serializers/pipeline_entity.rb1
-rw-r--r--app/services/ci/create_pipeline_service.rb40
-rw-r--r--app/services/concerns/update_visibility_level.rb15
-rw-r--r--app/services/groups/create_service.rb38
-rw-r--r--app/services/groups/update_service.rb27
-rw-r--r--app/services/projects/update_service.rb20
-rw-r--r--app/services/quick_actions/interpret_service.rb2
-rw-r--r--app/services/test_hooks/base_service.rb7
-rw-r--r--app/services/web_hook_service.rb2
-rw-r--r--app/views/admin/application_settings/_form.html.haml12
-rw-r--r--app/views/groups/edit.html.haml17
-rw-r--r--app/views/layouts/application.html.haml2
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml4
-rw-r--r--app/views/profiles/preferences/show.html.haml20
-rw-r--r--app/views/profiles/preferences/update.js.erb4
-rw-r--r--app/views/projects/_merge_request_merge_settings.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map.html.haml2
-rw-r--r--app/views/projects/blob/viewers/_route_map_loading.html.haml2
-rw-r--r--app/views/projects/commit/_pipelines_list.haml1
-rw-r--r--app/views/projects/edit.html.haml80
-rw-r--r--app/views/projects/merge_requests/index.html.haml2
-rw-r--r--app/views/projects/pipelines/index.html.haml34
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml41
-rw-r--r--app/views/projects/project_members/import.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/show.html.haml2
-rw-r--r--app/views/projects/show.html.haml3
-rw-r--r--app/views/projects/tree/show.html.haml2
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml15
-rw-r--r--app/views/shared/_new_project_item_select.html.haml2
-rw-r--r--app/views/shared/icons/_icon_autodevops.svg54
-rw-r--r--app/workers/git_garbage_collect_worker.rb34
-rw-r--r--changelogs/unreleased/23079-remove-default-scope-in-sortable.yml5
-rw-r--r--changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml5
-rw-r--r--changelogs/unreleased/32665-refactor-project-visibility-settings.yml5
-rw-r--r--changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml5
-rw-r--r--changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml5
-rw-r--r--changelogs/unreleased/37158-autodevops-banner.yml5
-rw-r--r--changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml5
-rw-r--r--changelogs/unreleased/37368-blob-viewer-on-mobile.yml5
-rw-r--r--changelogs/unreleased/docs-confidential-issue.yml5
-rw-r--r--changelogs/unreleased/fix-gem-security-updates.yml5
-rw-r--r--changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml5
-rw-r--r--changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml6
-rw-r--r--changelogs/unreleased/sh-add-grape-logging.yml5
-rw-r--r--changelogs/unreleased/support-additional-colors.yml5
-rw-r--r--changelogs/unreleased/zj-auto-devops-table.yml5
-rw-r--r--changelogs/unreleased/zj-update-rails-template.yml5
-rw-r--r--config/gitlab.yml.example7
-rw-r--r--config/initializers/0_inflections.rb7
-rw-r--r--config/initializers/1_settings.rb1
-rw-r--r--db/migrate/20170816234252_add_theme_id_to_users.rb10
-rw-r--r--db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb15
-rw-r--r--db/migrate/20170828093725_create_project_auto_dev_ops.rb19
-rw-r--r--db/migrate/20170831092813_add_config_source_to_pipelines.rb7
-rw-r--r--db/schema.rb16
-rw-r--r--doc/README.md3
-rw-r--r--doc/api/keys.md1
-rw-r--r--doc/api/session.md1
-rw-r--r--doc/api/users.md5
-rw-r--r--doc/ci/README.md4
-rw-r--r--doc/ci/autodeploy/index.md20
-rw-r--r--doc/ci/environments.md3
-rw-r--r--doc/development/img/manual_build_docs.pngbin0 -> 14869 bytes
-rw-r--r--doc/development/writing_documentation.md21
-rw-r--r--doc/install/database_mysql.md3
-rw-r--r--doc/install/kubernetes/gitlab_chart.md12
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md16
-rw-r--r--doc/install/kubernetes/gitlab_runner_chart.md4
-rw-r--r--doc/install/kubernetes/index.md61
-rw-r--r--doc/topics/autodevops/img/auto_devops_settings.pngbin0 -> 31851 bytes
-rw-r--r--doc/topics/autodevops/img/auto_monitoring.pngbin0 -> 89206 bytes
-rw-r--r--doc/topics/autodevops/img/guide_connect_cluster.pngbin0 -> 38724 bytes
-rw-r--r--doc/topics/autodevops/img/guide_integration.pngbin0 -> 44263 bytes
-rw-r--r--doc/topics/autodevops/img/guide_secret.pngbin0 -> 16233 bytes
-rw-r--r--doc/topics/autodevops/index.md342
-rw-r--r--doc/topics/autodevops/quick_start_guide.md95
-rw-r--r--doc/topics/index.md1
-rw-r--r--doc/user/group/img/share_with_group_lock.pngbin18257 -> 21541 bytes
-rw-r--r--doc/user/group/index.md32
-rw-r--r--doc/user/project/issues/confidential_issues.md24
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_index_page.pngbin8349 -> 107117 bytes
-rw-r--r--[-rwxr-xr-x]doc/user/project/issues/img/confidential_issues_issue_page.pngbin14230 -> 25354 bytes
-rwxr-xr-xdoc/user/project/issues/img/sidebar_confidential_issue.pngbin0 -> 10210 bytes
-rwxr-xr-xdoc/user/project/issues/img/sidebar_not_confidential_issue.pngbin0 -> 8163 bytes
-rw-r--r--doc/user/project/issues/img/turn_off_confidentiality.pngbin0 -> 27307 bytes
-rw-r--r--doc/user/project/issues/img/turn_on_confidentiality.pngbin0 -> 33499 bytes
-rw-r--r--doc/user/project/members/share_project_with_groups.md9
-rw-r--r--features/steps/project/fork.rb2
-rw-r--r--features/steps/project/project.rb1
-rw-r--r--features/steps/shared/project.rb2
-rw-r--r--lib/api/api.rb11
-rw-r--r--lib/api/broadcast_messages.rb2
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/api/groups.rb7
-rw-r--r--lib/api/helpers.rb2
-rw-r--r--lib/api/merge_request_diffs.rb2
-rw-r--r--lib/api/milestone_responses.rb2
-rw-r--r--lib/api/v3/merge_request_diffs.rb2
-rw-r--r--lib/api/v3/milestones.rb1
-rw-r--r--lib/api/v3/projects.rb2
-rw-r--r--lib/banzai/filter/sanitization_filter.rb25
-rw-r--r--lib/gitlab/exclusive_lease.rb10
-rw-r--r--lib/gitlab/gitaly_client.rb36
-rw-r--r--lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb19
-rw-r--r--lib/gitlab/import_export/import_export.yml3
-rw-r--r--lib/gitlab/import_export/relation_factory.rb1
-rw-r--r--lib/gitlab/middleware/go.rb33
-rw-r--r--lib/gitlab/themes.rb84
-rw-r--r--locale/gitlab.pot18
-rwxr-xr-xscripts/trigger-build-docs80
-rwxr-xr-xscripts/trigger-build-omnibus (renamed from scripts/trigger-build)0
-rw-r--r--spec/controllers/groups_controller_spec.rb124
-rw-r--r--spec/controllers/profiles/preferences_controller_spec.rb6
-rw-r--r--spec/factories/project_auto_devops.rb7
-rw-r--r--spec/features/copy_as_gfm_spec.rb2
-rw-r--r--spec/features/groups/share_lock_spec.rb62
-rw-r--r--spec/features/projects/edit_spec.rb31
-rw-r--r--spec/features/projects/environments/environments_spec.rb206
-rw-r--r--spec/features/projects/features_visibility_spec.rb43
-rw-r--r--spec/features/projects/group_links_spec.rb77
-rw-r--r--spec/features/projects/import_export/import_file_spec.rb7
-rw-r--r--spec/features/projects/members/groups_with_access_list_spec.rb (renamed from spec/features/projects/members/group_links_spec.rb)14
-rw-r--r--spec/features/projects/members/share_with_group_spec.rb191
-rw-r--r--spec/features/projects/settings/merge_requests_settings_spec.rb6
-rw-r--r--spec/features/projects/settings/pipelines_settings_spec.rb10
-rw-r--r--spec/features/projects/settings/visibility_settings_spec.rb19
-rw-r--r--spec/finders/group_members_finder_spec.rb4
-rw-r--r--spec/finders/members_finder_spec.rb2
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/user/login.json1
-rw-r--r--spec/helpers/auto_devops_helper_spec.rb59
-rw-r--r--spec/helpers/commits_helper_spec.rb22
-rw-r--r--spec/helpers/groups_helper_spec.rb93
-rw-r--r--spec/helpers/preferences_helper_spec.rb30
-rw-r--r--spec/helpers/projects_helper_spec.rb29
-rw-r--r--spec/javascripts/commit/pipelines/pipelines_spec.js4
-rw-r--r--spec/javascripts/monitoring/graph/legend_spec.js9
-rw-r--r--spec/javascripts/monitoring/graph_path_spec.js (renamed from spec/javascripts/monitoring/monitoring_paths_spec.js)23
-rw-r--r--spec/javascripts/monitoring/mock_data.js8
-rw-r--r--spec/javascripts/monitoring/utils/multiple_time_series_spec.js15
-rw-r--r--spec/javascripts/notes_spec.js14
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js27
-rw-r--r--spec/javascripts/pipelines/pipelines_table_row_spec.js2
-rw-r--r--spec/javascripts/pipelines/pipelines_table_spec.js3
-rw-r--r--spec/javascripts/projects_dropdown/components/projects_list_search_spec.js2
-rw-r--r--spec/javascripts/projects_dropdown/components/search_spec.js2
-rw-r--r--spec/javascripts/user_callout_spec.js13
-rw-r--r--spec/lib/banzai/filter/sanitization_filter_spec.rb32
-rw-r--r--spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb12
-rw-r--r--spec/lib/gitlab/exclusive_lease_spec.rb12
-rw-r--r--spec/lib/gitlab/gitaly_client_spec.rb16
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/lib/gitlab/import_export/safe_model_attributes.yml8
-rw-r--r--spec/lib/gitlab/middleware/go_spec.rb18
-rw-r--r--spec/lib/gitlab/themes_spec.rb48
-rw-r--r--spec/migrations/convert_custom_notification_settings_to_columns_spec.rb6
-rw-r--r--spec/models/application_setting_spec.rb1
-rw-r--r--spec/models/ci/build_spec.rb24
-rw-r--r--spec/models/ci/pipeline_spec.rb114
-rw-r--r--spec/models/environment_spec.rb22
-rw-r--r--spec/models/namespace_spec.rb114
-rw-r--r--spec/models/project_auto_devops_spec.rb23
-rw-r--r--spec/models/project_spec.rb174
-rw-r--r--spec/models/user_spec.rb4
-rw-r--r--spec/policies/group_policy_spec.rb94
-rw-r--r--spec/requests/api/groups_spec.rb24
-rw-r--r--spec/requests/api/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb4
-rw-r--r--spec/requests/api/v3/merge_request_diffs_spec.rb2
-rw-r--r--spec/requests/projects/cycle_analytics_events_spec.rb2
-rw-r--r--spec/serializers/environment_entity_spec.rb4
-rw-r--r--spec/serializers/pipeline_entity_spec.rb2
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb4
-rw-r--r--spec/services/groups/create_service_spec.rb43
-rw-r--r--spec/services/groups/update_service_spec.rb34
-rw-r--r--spec/services/notification_service_spec.rb4
-rw-r--r--spec/services/projects/housekeeping_service_spec.rb1
-rw-r--r--spec/services/test_hooks/project_service_spec.rb27
-rw-r--r--spec/services/test_hooks/system_service_spec.rb9
-rw-r--r--spec/services/web_hook_service_spec.rb2
-rw-r--r--spec/support/controllers/githubish_import_controller_shared_examples.rb14
-rw-r--r--spec/support/gitlab_stubs/session.json2
-rw-r--r--spec/support/gitlab_stubs/user.json4
-rw-r--r--spec/views/groups/edit.html.haml_spec.rb116
-rw-r--r--spec/views/projects/edit.html.haml_spec.rb11
-rw-r--r--spec/workers/git_garbage_collect_worker_spec.rb121
-rw-r--r--spec/workers/post_receive_spec.rb2
-rw-r--r--vendor/project_templates/rails.tar.gzbin24777 -> 25063 bytes
270 files changed, 5083 insertions, 960 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 7b42e661dff..dadce073309 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -40,6 +40,7 @@ stages:
- test
- post-test
- pages
+ - post-cleanup
# Predefined scopes
.dedicated-runner: &dedicated-runner
@@ -153,8 +154,7 @@ stages:
- master@gitlab/gitlabhq
- master@gitlab/gitlab-ee
-# Trigger a package build on omnibus-gitlab repository
-
+# Trigger a package build in omnibus-gitlab repository
build-package:
image: ruby:2.3-alpine
before_script: []
@@ -166,11 +166,47 @@ build-package:
cache: {}
when: manual
script:
- - scripts/trigger-build
+ - scripts/trigger-build-omnibus
only:
- //@gitlab-org/gitlab-ce
- //@gitlab-org/gitlab-ee
+# Review docs base
+.review-docs: &review-docs
+ image: ruby:2.4-alpine
+ before_script: []
+ services: []
+ variables:
+ SETUP_DB: "false"
+ USE_BUNDLE_INSTALL: "false"
+ cache: {}
+ when: manual
+ only:
+ - branches
+
+# Trigger a docs build in gitlab-docs
+# Useful to preview the docs changes live
+review-docs-deploy:
+ <<: *review-docs
+ stage: build
+ environment:
+ name: review-docs/$CI_COMMIT_REF_NAME
+ on_stop: review-docs-cleanup
+ script:
+ - gem install gitlab --no-doc
+ - scripts/trigger-build-docs deploy
+
+# Cleanup remote environment of gitlab-docs
+review-docs-cleanup:
+ <<: *review-docs
+ stage: post-cleanup
+ environment:
+ name: review-docs/$CI_COMMIT_REF_NAME
+ action: stop
+ script:
+ - gem install gitlab --no-doc
+ - scripts/trigger-build-docs cleanup
+
# Retrieve knapsack and rspec_flaky reports
retrieve-tests-metadata:
<<: *tests-metadata-state
diff --git a/.rubocop.yml b/.rubocop.yml
index 16f2e4484fc..4640681379a 100644
--- a/.rubocop.yml
+++ b/.rubocop.yml
@@ -643,7 +643,7 @@ Metrics/ClassLength:
# of test cases needed to validate a method.
Metrics/CyclomaticComplexity:
Enabled: true
- Max: 15
+ Max: 14
# Limit lines to 80 characters.
Metrics/LineLength:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index a02b6594fad..4cedfa60b3e 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -2,6 +2,16 @@
documentation](doc/development/changelog.md) for instructions on adding your own
entry.
+## 9.5.4 (2017-09-06)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Prevent a persistent XSS in the commit author block.
+- Fix XSS issue in go-get handling.
+- Resolve CSRF token leakage via pathname manipulation on environments page.
+- Fixes race condition in project uploads.
+- Disallow arbitrary properties in `th` and `td` `style` attributes.
+- Disallow the `name` attribute on all user-provided markup.
+
## 9.5.3 (2017-09-03)
- [SECURITY] Filter additional secrets from Rails logs.
@@ -203,6 +213,18 @@ entry.
- Use a specialized class for querying events to improve performance.
- Update build badges to be pipeline badges and display passing instead of success.
+## 9.4.6 (2017-09-06)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Prevent a persistent XSS in the commit author block.
+- Fix XSS issue in go-get handling.
+- Remove hidden symlinks from project import files.
+- Fixes race condition in project uploads.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+- Disallow arbitrary properties in `th` and `td` `style` attributes.
+- Resolve CSRF token leakage via pathname manipulation on environments page.
+- Disallow the `name` attribute on all user-provided markup.
+
## 9.4.5 (2017-08-14)
- Fix deletion of deploy keys linked to other projects. !13162
@@ -453,6 +475,24 @@ entry.
- Log rescued exceptions to Sentry.
- Remove remaining N+1 queries in merge requests API with emojis and labels.
+## 9.3.11 (2017-09-06)
+
+- [SECURITY] Upgrade mail and nokogiri gems due to security issues. !13662 (Markus Koller)
+- [SECURITY] Prevent a persistent XSS in the commit author block.
+- Improve support for external issue references. !12485
+- Use uploads/system directory for personal snippets.
+- Remove uploads/appearance symlink. A leftover from a previous migration.
+- Fix XSS issue in go-get handling.
+- Remove hidden symlinks from project import files.
+- Fix an infinite loop when handling user-supplied regular expressions.
+- Fixes race condition in project uploads.
+- Fixes race condition in project uploads.
+- Disallow Git URLs that include a username or hostname beginning with a non-alphanumeric character.
+- Disallow arbitrary properties in `th` and `td` `style` attributes.
+- Resolve CSRF token leakage via pathname manipulation on environments page.
+- Disallow the `name` attribute on all user-provided markup.
+- Renders 404 if given project is not readable by the user on Todos dashboard.
+
## 9.3.10 (2017-08-09)
- Remove hidden symlinks from project import files.
diff --git a/GITLAB_PAGES_VERSION b/GITLAB_PAGES_VERSION
index 8f0916f768f..4b9fcbec101 100644
--- a/GITLAB_PAGES_VERSION
+++ b/GITLAB_PAGES_VERSION
@@ -1 +1 @@
-0.5.0
+0.5.1
diff --git a/Gemfile b/Gemfile
index c5eb4ab51ca..a022319ae2c 100644
--- a/Gemfile
+++ b/Gemfile
@@ -407,3 +407,4 @@ gem 'flipper-active_record', '~> 0.10.2'
# Structured logging
gem 'lograge', '~> 0.5'
+gem 'grape_logging', '~> 1.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9fbf08d80e2..d7e1c7581d5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -355,6 +355,8 @@ GEM
activesupport
grape (>= 0.16.0)
rake
+ grape_logging (1.6.0)
+ grape
grpc (1.4.5)
google-protobuf (~> 3.1)
googleauth (~> 0.5.1)
@@ -1035,6 +1037,7 @@ DEPENDENCIES
grape (~> 1.0)
grape-entity (~> 0.6.0)
grape-route-helpers (~> 2.1.0)
+ grape_logging (~> 1.6)
haml_lint (~> 0.26.0)
hamlit (~> 2.6.1)
hashie-forbidden_attributes
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
index 687f09882a7..16c5d0fa344 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_bundle.js
@@ -35,6 +35,7 @@ document.addEventListener('DOMContentLoaded', () => {
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
pipelineTableViewEl.appendChild(table.$el);
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.vue b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
index dd751ec97a8..c931e1e0ea5 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.vue
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.vue
@@ -13,6 +13,10 @@
type: String,
required: true,
},
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
mixins: [
pipelinesMixin,
@@ -95,6 +99,7 @@
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
+ :auto-devops-help-path="autoDevopsHelpPath"
/>
</div>
</div>
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 6db0b18ae5a..f3b537c83e2 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -160,6 +160,9 @@ import initChangesDropdown from './init_changes_dropdown';
const filteredSearchManager = new gl.FilteredSearchManager(page === 'projects:issues:index' ? 'issues' : 'merge_requests');
filteredSearchManager.setup();
}
+ if (page === 'projects:merge_requests:index') {
+ new UserCallout({ setCalloutPerProject: true });
+ }
const pagePrefix = page === 'projects:merge_requests:index' ? 'merge_request_' : 'issue_';
IssuableIndex.init(pagePrefix);
@@ -342,6 +345,7 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects:show':
shortcut_handler = new ShortcutsNavigation();
new NotificationsForm();
+ new UserCallout({ setCalloutPerProject: true });
if ($('#tree-slider').length) new TreeView();
if ($('.blob-viewer').length) new BlobViewer();
@@ -361,6 +365,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'projects:pipelines:new':
new NewBranchForm($('.js-new-pipeline-form'));
break;
+ case 'projects:pipelines:index':
+ new UserCallout({ setCalloutPerProject: true });
+ break;
case 'projects:pipelines:builds':
case 'projects:pipelines:failures':
case 'projects:pipelines:show':
@@ -418,6 +425,7 @@ import initChangesDropdown from './init_changes_dropdown';
new TreeView();
new BlobViewer();
new NewCommitForm($('.js-create-dir-form'));
+ new UserCallout({ setCalloutPerProject: true });
$('#tree-slider').waitForImages(function() {
gl.utils.ajaxGet(document.querySelector('.js-tree-content').dataset.logsPath);
});
@@ -569,6 +577,9 @@ import initChangesDropdown from './init_changes_dropdown';
case 'edit':
shortcut_handler = new ShortcutsNavigation();
new ProjectNew();
+ import(/* webpackChunkName: 'project_permissions' */ './projects/permissions')
+ .then(permissions => permissions.default())
+ .catch(() => {});
break;
case 'new':
new ProjectNew();
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index 91ed8c8467f..f54d573db6e 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -111,11 +111,11 @@ export default {
},
methods: {
- toggleFolder(folder, folderUrl) {
+ toggleFolder(folder) {
this.store.toggleFolder(folder);
if (!folder.isOpen) {
- this.fetchChildEnvironments(folder, folderUrl, true);
+ this.fetchChildEnvironments(folder, true);
}
},
@@ -143,10 +143,10 @@ export default {
.catch(this.errorCallback);
},
- fetchChildEnvironments(folder, folderUrl, showLoader = false) {
+ fetchChildEnvironments(folder, showLoader = false) {
this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', showLoader);
- this.service.getFolderContent(folderUrl)
+ this.service.getFolderContent(folder.folder_path)
.then(resp => resp.json())
.then(response => this.store.setfolderContent(folder, response.environments))
.then(() => this.store.updateEnvironmentProp(folder, 'isLoadingFolderContent', false))
@@ -173,12 +173,7 @@ export default {
// We need to verify if any folder is open to also update it
const openFolders = this.store.getOpenFolders();
if (openFolders.length) {
- openFolders.forEach((folder) => {
- // TODO - Move this to the backend
- const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
-
- return this.fetchChildEnvironments(folder, folderUrl);
- });
+ openFolders.forEach(folder => this.fetchChildEnvironments(folder));
}
},
diff --git a/app/assets/javascripts/environments/components/environment_item.vue b/app/assets/javascripts/environments/components/environment_item.vue
index d8b1b2f1b92..6de01fa53d0 100644
--- a/app/assets/javascripts/environments/components/environment_item.vue
+++ b/app/assets/javascripts/environments/components/environment_item.vue
@@ -410,20 +410,11 @@ export default {
this.hasStopAction ||
this.canRetry;
},
-
- /**
- * Constructs folder URL based on the current location and the folder id.
- *
- * @return {String}
- */
- folderUrl() {
- return `${window.location.pathname}/folders/${this.model.folderName}`;
- },
},
methods: {
onClickFolder() {
- eventHub.$emit('toggleFolder', this.model, this.folderUrl);
+ eventHub.$emit('toggleFolder', this.model);
},
},
};
diff --git a/app/assets/javascripts/lib/utils/number_utils.js b/app/assets/javascripts/lib/utils/number_utils.js
index 57394097944..917a45eb06b 100644
--- a/app/assets/javascripts/lib/utils/number_utils.js
+++ b/app/assets/javascripts/lib/utils/number_utils.js
@@ -13,7 +13,7 @@ export function formatRelevantDigits(number) {
let relevantDigits = 0;
let formattedNumber = '';
if (!isNaN(Number(number))) {
- digitsLeft = number.split('.')[0];
+ digitsLeft = number.toString().split('.')[0];
switch (digitsLeft.length) {
case 1:
relevantDigits = 3;
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 3b3620fe61b..0c3c877ff15 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -243,6 +243,7 @@ import bp from './breakpoints';
propsData: {
endpoint: pipelineTableViewEl.dataset.endpoint,
helpPagePath: pipelineTableViewEl.dataset.helpPagePath,
+ autoDevopsHelpPath: pipelineTableViewEl.dataset.helpAutoDevopsPath,
},
}).$mount();
diff --git a/app/assets/javascripts/monitoring/components/graph.vue b/app/assets/javascripts/monitoring/components/graph.vue
index cde2ff7ca2a..6b3e341f936 100644
--- a/app/assets/javascripts/monitoring/components/graph.vue
+++ b/app/assets/javascripts/monitoring/components/graph.vue
@@ -3,7 +3,7 @@
import GraphLegend from './graph/legend.vue';
import GraphFlag from './graph/flag.vue';
import GraphDeployment from './graph/deployment.vue';
- import monitoringPaths from './monitoring_paths.vue';
+ import GraphPath from './graph_path.vue';
import MonitoringMixin from '../mixins/monitoring_mixins';
import eventHub from '../event_hub';
import measurements from '../utils/measurements';
@@ -40,8 +40,6 @@
graphHeightOffset: 120,
margin: {},
unitOfDisplay: '',
- areaColorRgb: '#8fbce8',
- lineColorRgb: '#1f78d1',
yAxisLabel: '',
legendTitle: '',
reducedDeploymentData: [],
@@ -63,7 +61,7 @@
GraphLegend,
GraphFlag,
GraphDeployment,
- monitoringPaths,
+ GraphPath,
},
computed: {
@@ -143,7 +141,7 @@
},
renderAxesPaths() {
- this.timeSeries = createTimeSeries(this.graphData.queries[0].result,
+ this.timeSeries = createTimeSeries(this.graphData.queries[0],
this.graphWidth,
this.graphHeight,
this.graphHeightOffset);
@@ -162,7 +160,7 @@
const xAxis = d3.svg.axis()
.scale(axisXScale)
- .ticks(measurements.xTicks)
+ .ticks(d3.time.minute, 60)
.tickFormat(timeScaleFormat)
.orient('bottom');
@@ -238,7 +236,7 @@
class="graph-data"
:viewBox="innerViewBox"
ref="graphData">
- <monitoring-paths
+ <graph-path
v-for="(path, index) in timeSeries"
:key="index"
:generated-line-path="path.linePath"
@@ -246,7 +244,7 @@
:line-color="path.lineColor"
:area-color="path.areaColor"
/>
- <monitoring-deployment
+ <graph-deployment
:show-deploy-info="showDeployInfo"
:deployment-data="reducedDeploymentData"
:graph-height="graphHeight"
diff --git a/app/assets/javascripts/monitoring/components/graph/legend.vue b/app/assets/javascripts/monitoring/components/graph/legend.vue
index a43dad8e601..dbc48c63747 100644
--- a/app/assets/javascripts/monitoring/components/graph/legend.vue
+++ b/app/assets/javascripts/monitoring/components/graph/legend.vue
@@ -81,6 +81,13 @@
formatMetricUsage(series) {
return `${formatRelevantDigits(series.values[this.currentDataIndex].value)} ${this.unitOfDisplay}`;
},
+
+ createSeriesString(index, series) {
+ if (series.metricTag) {
+ return `${series.metricTag} ${this.formatMetricUsage(series)}`;
+ }
+ return `${this.legendTitle} series ${index + 1} ${this.formatMetricUsage(series)}`;
+ },
},
mounted() {
this.$nextTick(() => {
@@ -164,7 +171,7 @@
ref="legendTitleSvg"
x="38"
:y="graphHeight - 30">
- {{legendTitle}} Series {{index + 1}} {{formatMetricUsage(series)}}
+ {{createSeriesString(index, series)}}
</text>
<text
v-else
diff --git a/app/assets/javascripts/monitoring/components/monitoring_paths.vue b/app/assets/javascripts/monitoring/components/graph_path.vue
index 043f1bf66bb..043f1bf66bb 100644
--- a/app/assets/javascripts/monitoring/components/monitoring_paths.vue
+++ b/app/assets/javascripts/monitoring/components/graph_path.vue
diff --git a/app/assets/javascripts/monitoring/utils/multiple_time_series.js b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
index 05d551e917c..3cbe06d8fd6 100644
--- a/app/assets/javascripts/monitoring/utils/multiple_time_series.js
+++ b/app/assets/javascripts/monitoring/utils/multiple_time_series.js
@@ -1,8 +1,37 @@
import d3 from 'd3';
import _ from 'underscore';
-export default function createTimeSeries(seriesData, graphWidth, graphHeight, graphHeightOffset) {
- const maxValues = seriesData.map((timeSeries, index) => {
+const defaultColorPalette = {
+ blue: ['#1f78d1', '#8fbce8'],
+ orange: ['#fc9403', '#feca81'],
+ red: ['#db3b21', '#ed9d90'],
+ green: ['#1aaa55', '#8dd5aa'],
+ purple: ['#6666c4', '#d1d1f0'],
+};
+
+const defaultColorOrder = ['blue', 'orange', 'red', 'green', 'purple'];
+
+export default function createTimeSeries(queryData, graphWidth, graphHeight, graphHeightOffset) {
+ let usedColors = [];
+
+ function pickColor(name) {
+ let pick;
+ if (name && defaultColorPalette[name]) {
+ pick = name;
+ } else {
+ const unusedColors = _.difference(defaultColorOrder, usedColors);
+ if (unusedColors.length > 0) {
+ pick = unusedColors[0];
+ } else {
+ usedColors = [];
+ pick = defaultColorOrder[0];
+ }
+ }
+ usedColors.push(pick);
+ return defaultColorPalette[pick];
+ }
+
+ const maxValues = queryData.result.map((timeSeries, index) => {
const maxValue = d3.max(timeSeries.values.map(d => d.value));
return {
maxValue,
@@ -12,10 +41,11 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
const maxValueFromSeries = _.max(maxValues, val => val.maxValue);
- let timeSeriesNumber = 1;
- let lineColor = '#1f78d1';
- let areaColor = '#8fbce8';
- return seriesData.map((timeSeries) => {
+ return queryData.result.map((timeSeries, timeSeriesNumber) => {
+ let metricTag = '';
+ let lineColor = '';
+ let areaColor = '';
+
const timeSeriesScaleX = d3.time.scale()
.range([0, graphWidth - 70]);
@@ -23,49 +53,30 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
.range([graphHeight - graphHeightOffset, 0]);
timeSeriesScaleX.domain(d3.extent(timeSeries.values, d => d.time));
+ timeSeriesScaleX.ticks(d3.time.minute, 60);
timeSeriesScaleY.domain([0, maxValueFromSeries.maxValue]);
const lineFunction = d3.svg.line()
+ .interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y(d => timeSeriesScaleY(d.value));
const areaFunction = d3.svg.area()
+ .interpolate('linear')
.x(d => timeSeriesScaleX(d.time))
.y0(graphHeight - graphHeightOffset)
- .y1(d => timeSeriesScaleY(d.value))
- .interpolate('linear');
-
- switch (timeSeriesNumber) {
- case 1:
- lineColor = '#1f78d1';
- areaColor = '#8fbce8';
- break;
- case 2:
- lineColor = '#fc9403';
- areaColor = '#feca81';
- break;
- case 3:
- lineColor = '#db3b21';
- areaColor = '#ed9d90';
- break;
- case 4:
- lineColor = '#1aaa55';
- areaColor = '#8dd5aa';
- break;
- case 5:
- lineColor = '#6666c4';
- areaColor = '#d1d1f0';
- break;
- default:
- lineColor = '#1f78d1';
- areaColor = '#8fbce8';
- break;
- }
+ .y1(d => timeSeriesScaleY(d.value));
- if (timeSeriesNumber <= 5) {
- timeSeriesNumber = timeSeriesNumber += 1;
+ const timeSeriesMetricLabel = timeSeries.metric[Object.keys(timeSeries.metric)[0]];
+ const seriesCustomizationData = queryData.series != null &&
+ _.findWhere(queryData.series[0].when,
+ { value: timeSeriesMetricLabel });
+ if (seriesCustomizationData != null) {
+ metricTag = seriesCustomizationData.value || timeSeriesMetricLabel;
+ [lineColor, areaColor] = pickColor(seriesCustomizationData.color);
} else {
- timeSeriesNumber = 1;
+ metricTag = timeSeriesMetricLabel || `series ${timeSeriesNumber + 1}`;
+ [lineColor, areaColor] = pickColor();
}
return {
@@ -75,6 +86,7 @@ export default function createTimeSeries(seriesData, graphWidth, graphHeight, gr
values: timeSeries.values,
lineColor,
areaColor,
+ metricTag,
};
});
}
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index a09270d6d24..f5f7bb4653d 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -1272,16 +1272,16 @@ export default class Notes {
`<li id="${uniqueId}" class="note being-posted fade-in-half timeline-entry">
<div class="timeline-entry-inner">
<div class="timeline-icon">
- <a href="/${currentUsername}">
+ <a href="/${_.escape(currentUsername)}">
<img class="avatar s40" src="${currentUserAvatar}" />
</a>
</div>
<div class="timeline-content ${discussionClass}">
<div class="note-header">
<div class="note-header-info">
- <a href="/${currentUsername}">
- <span class="hidden-xs">${currentUserFullname}</span>
- <span class="note-headline-light">@${currentUsername}</span>
+ <a href="/${_.escape(currentUsername)}">
+ <span class="hidden-xs">${_.escape(currentUsername)}</span>
+ <span class="note-headline-light">${_.escape(currentUsername)}</span>
</a>
</div>
</div>
@@ -1295,6 +1295,9 @@ export default class Notes {
</li>`
);
+ $tempNote.find('.hidden-xs').text(_.escape(currentUserFullname));
+ $tempNote.find('.note-headline-light').text(`@${_.escape(currentUsername)}`);
+
return $tempNote;
}
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index 2ca5ac2912f..f0b44dfa6d8 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -1,29 +1,45 @@
<script>
-import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
-import tooltip from '../../vue_shared/directives/tooltip';
+ import userAvatarLink from '../../vue_shared/components/user_avatar/user_avatar_link.vue';
+ import tooltip from '../../vue_shared/directives/tooltip';
+ import popover from '../../vue_shared/directives/popover';
-export default {
- props: {
- pipeline: {
- type: Object,
- required: true,
+ export default {
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
- },
- components: {
- userAvatarLink,
- },
- directives: {
- tooltip,
- },
- computed: {
- user() {
- return this.pipeline.user;
+ components: {
+ userAvatarLink,
},
- },
-};
+ directives: {
+ tooltip,
+ popover,
+ },
+ computed: {
+ user() {
+ return this.pipeline.user;
+ },
+ popoverOptions() {
+ return {
+ html: true,
+ delay: { hide: 600 },
+ trigger: 'hover',
+ placement: 'top',
+ title: '<div class="autodevops-title">This pipeline makes use of a predefined CI/CD configuration enabled by <b>Auto DevOps.</b></div>',
+ content: `<a class="autodevops-link" href="${this.autoDevopsHelpPath}" target="_blank" rel="noopener noreferrer nofollow">Learn more about Auto DevOps</a>`,
+ };
+ },
+ },
+ };
</script>
<template>
- <div class="table-section section-15 hidden-xs hidden-sm">
+ <div class="table-section section-15 hidden-xs hidden-sm pipeline-tags">
<a
:href="pipeline.path"
class="js-pipeline-url-link">
@@ -57,6 +73,13 @@ export default {
:title="pipeline.yaml_errors">
yaml invalid
</span>
+ <a
+ v-if="pipeline.flags.auto_devops"
+ class="js-pipeline-url-autodevops label label-info autodevops-badge"
+ v-popover="popoverOptions"
+ role="button">
+ Auto DevOps
+ </a>
<span
v-if="pipeline.flags.stuck"
class="js-pipeline-url-stuck label label-warning">
diff --git a/app/assets/javascripts/pipelines/components/pipelines.vue b/app/assets/javascripts/pipelines/components/pipelines.vue
index 010063a0240..5e6d6b2fbdc 100644
--- a/app/assets/javascripts/pipelines/components/pipelines.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines.vue
@@ -25,8 +25,8 @@
return {
endpoint: pipelinesData.endpoint,
- cssClass: pipelinesData.cssClass,
helpPagePath: pipelinesData.helpPagePath,
+ autoDevopsPath: pipelinesData.helpAutoDevopsPath,
newPipelinePath: pipelinesData.newPipelinePath,
canCreatePipeline: pipelinesData.canCreatePipeline,
allPath: pipelinesData.allPath,
@@ -139,9 +139,7 @@
};
</script>
<template>
- <div
- class="pipelines-container"
- :class="cssClass">
+ <div class="pipelines-container">
<div
class="top-area scrolling-tabs-container inner-page-scroll-tabs"
v-if="!isLoading && !shouldRenderEmptyState">
@@ -200,6 +198,7 @@
<pipelines-table-component
:pipelines="state.pipelines"
:update-graph-dropdown="updateGraphDropdown"
+ :auto-devops-help-path="autoDevopsPath"
/>
</div>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table.vue b/app/assets/javascripts/pipelines/components/pipelines_table.vue
index 5088d92209f..7aa0c0e8a7f 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table.vue
@@ -17,6 +17,10 @@
required: false,
default: false,
},
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
components: {
pipelinesTableRowComponent,
@@ -54,6 +58,7 @@
:key="model.id"
:pipeline="model"
:update-graph-dropdown="updateGraphDropdown"
+ :auto-devops-help-path="autoDevopsHelpPath"
/>
</div>
</template>
diff --git a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
index c3f1c426d8a..5b9bb6c3750 100644
--- a/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
+++ b/app/assets/javascripts/pipelines/components/pipelines_table_row.vue
@@ -25,6 +25,10 @@ export default {
required: false,
default: false,
},
+ autoDevopsHelpPath: {
+ type: String,
+ required: true,
+ },
},
components: {
asyncButtonComponent,
@@ -218,7 +222,10 @@ export default {
</div>
</div>
- <pipeline-url :pipeline="pipeline" />
+ <pipeline-url
+ :pipeline="pipeline"
+ :auto-devops-help-path="autoDevopsHelpPath"
+ />
<div class="table-section section-25">
<div
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
new file mode 100644
index 00000000000..80c5d39f736
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_setting.vue
@@ -0,0 +1,104 @@
+<script>
+import projectFeatureToggle from './project_feature_toggle.vue';
+
+export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ options: {
+ type: Array,
+ required: false,
+ default: () => [],
+ },
+ value: {
+ type: Number,
+ required: false,
+ default: 0,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ components: {
+ projectFeatureToggle,
+ },
+
+ computed: {
+ featureEnabled() {
+ return this.value !== 0;
+ },
+
+ displayOptions() {
+ if (this.featureEnabled) {
+ return this.options;
+ }
+ return [
+ [0, 'Enable feature to choose access level'],
+ ];
+ },
+
+ displaySelectInput() {
+ return this.disabledInput || !this.featureEnabled || this.displayOptions.length < 2;
+ },
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature(featureEnabled) {
+ if (featureEnabled === false || this.options.length < 1) {
+ this.$emit('change', 0);
+ } else {
+ const [firstOptionValue] = this.options[this.options.length - 1];
+ this.$emit('change', firstOptionValue);
+ }
+ },
+
+ selectOption(e) {
+ this.$emit('change', Number(e.target.value));
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="project-feature-controls" :data-for="name">
+ <input
+ v-if="name"
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <project-feature-toggle
+ :value="featureEnabled"
+ @change="toggleFeature"
+ :disabledInput="disabledInput"
+ />
+ <div class="select-wrapper">
+ <select
+ class="form-control project-repo-select select-control"
+ @change="selectOption"
+ :disabled="displaySelectInput"
+ >
+ <option
+ v-for="[optionValue, optionName] in displayOptions"
+ :key="optionValue"
+ :value="optionValue"
+ :selected="optionValue === value"
+ >
+ {{optionName}}
+ </option>
+ </select>
+ <i aria-hidden="true" class="fa fa-chevron-down"></i>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
new file mode 100644
index 00000000000..2403c60186a
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/project_feature_toggle.vue
@@ -0,0 +1,51 @@
+<script>
+export default {
+ props: {
+ name: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ value: {
+ type: Boolean,
+ required: true,
+ },
+ disabledInput: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ },
+
+ model: {
+ prop: 'value',
+ event: 'change',
+ },
+
+ methods: {
+ toggleFeature() {
+ if (!this.disabledInput) this.$emit('change', !this.value);
+ },
+ },
+};
+</script>
+
+<template>
+ <label class="toggle-wrapper">
+ <input
+ v-if="name"
+ type="hidden"
+ :name="name"
+ :value="value"
+ />
+ <button
+ type="button"
+ aria-label="Toggle"
+ class="project-feature-toggle"
+ data-enabled-text="Enabled"
+ data-disabled-text="Disabled"
+ :class="{ checked: value, disabled: disabledInput }"
+ @click="toggleFeature"
+ />
+ </label>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/components/project_setting_row.vue b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue
new file mode 100644
index 00000000000..6140d74fea8
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/project_setting_row.vue
@@ -0,0 +1,36 @@
+<script>
+export default {
+ props: {
+ label: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ helpText: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+};
+</script>
+
+<template>
+ <div class="project-feature-row">
+ <label v-if="label" class="label-light">
+ {{label}}
+ <a v-if="helpPath" :href="helpPath" target="_blank">
+ <i aria-hidden="true" data-hidden="true" class="fa fa-question-circle"></i>
+ </a>
+ </label>
+ <span v-if="helpText" class="help-block">
+ {{helpText}}
+ </span>
+ <slot />
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/components/settings_panel.vue b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
new file mode 100644
index 00000000000..326d9105666
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/components/settings_panel.vue
@@ -0,0 +1,312 @@
+<script>
+import projectFeatureSetting from './project_feature_setting.vue';
+import projectFeatureToggle from './project_feature_toggle.vue';
+import projectSettingRow from './project_setting_row.vue';
+import { visibilityOptions, visibilityLevelDescriptions } from '../constants';
+import { toggleHiddenClassBySelector } from '../external';
+
+export default {
+ props: {
+ currentSettings: {
+ type: Object,
+ required: true,
+ },
+ canChangeVisibilityLevel: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ allowedVisibilityOptions: {
+ type: Array,
+ required: false,
+ default: () => [0, 10, 20],
+ },
+ lfsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ registryAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ visibilityHelpPath: {
+ type: String,
+ required: false,
+ },
+ lfsHelpPath: {
+ type: String,
+ required: false,
+ },
+ registryHelpPath: {
+ type: String,
+ required: false,
+ },
+ },
+
+ data() {
+ const defaults = {
+ visibilityOptions,
+ visibilityLevel: visibilityOptions.PUBLIC,
+ issuesAccessLevel: 20,
+ repositoryAccessLevel: 20,
+ mergeRequestsAccessLevel: 20,
+ buildsAccessLevel: 20,
+ wikiAccessLevel: 20,
+ snippetsAccessLevel: 20,
+ containerRegistryEnabled: true,
+ lfsEnabled: true,
+ requestAccessEnabled: true,
+ highlightChangesClass: false,
+ };
+
+ return { ...defaults, ...this.currentSettings };
+ },
+
+ components: {
+ projectFeatureSetting,
+ projectFeatureToggle,
+ projectSettingRow,
+ },
+
+ computed: {
+ featureAccessLevelOptions() {
+ const options = [
+ [10, 'Only Project Members'],
+ ];
+ if (this.visibilityLevel !== visibilityOptions.PRIVATE) {
+ options.push([20, 'Everyone With Access']);
+ }
+ return options;
+ },
+
+ repoFeatureAccessLevelOptions() {
+ return this.featureAccessLevelOptions.filter(
+ ([value]) => value <= this.repositoryAccessLevel,
+ );
+ },
+
+ repositoryEnabled() {
+ return this.repositoryAccessLevel > 0;
+ },
+
+ visibilityLevelDescription() {
+ return visibilityLevelDescriptions[this.visibilityLevel];
+ },
+ },
+
+ methods: {
+ highlightChanges() {
+ this.highlightChangesClass = true;
+ this.$nextTick(() => {
+ this.highlightChangesClass = false;
+ });
+ },
+
+ visibilityAllowed(option) {
+ return this.allowedVisibilityOptions.includes(option);
+ },
+ },
+
+ watch: {
+ visibilityLevel(value, oldValue) {
+ if (value === visibilityOptions.PRIVATE) {
+ // when private, features are restricted to "only team members"
+ this.issuesAccessLevel = Math.min(10, this.issuesAccessLevel);
+ this.repositoryAccessLevel = Math.min(10, this.repositoryAccessLevel);
+ this.mergeRequestsAccessLevel = Math.min(10, this.mergeRequestsAccessLevel);
+ this.buildsAccessLevel = Math.min(10, this.buildsAccessLevel);
+ this.wikiAccessLevel = Math.min(10, this.wikiAccessLevel);
+ this.snippetsAccessLevel = Math.min(10, this.snippetsAccessLevel);
+ this.highlightChanges();
+ } else if (oldValue === visibilityOptions.PRIVATE) {
+ // if changing away from private, make enabled features more permissive
+ if (this.issuesAccessLevel > 0) this.issuesAccessLevel = 20;
+ if (this.repositoryAccessLevel > 0) this.repositoryAccessLevel = 20;
+ if (this.mergeRequestsAccessLevel > 0) this.mergeRequestsAccessLevel = 20;
+ if (this.buildsAccessLevel > 0) this.buildsAccessLevel = 20;
+ if (this.wikiAccessLevel > 0) this.wikiAccessLevel = 20;
+ if (this.snippetsAccessLevel > 0) this.snippetsAccessLevel = 20;
+ this.highlightChanges();
+ }
+ },
+
+ repositoryAccessLevel(value, oldValue) {
+ if (value < oldValue) {
+ // sub-features cannot have more premissive access level
+ this.mergeRequestsAccessLevel = Math.min(this.mergeRequestsAccessLevel, value);
+ this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
+
+ if (value === 0) {
+ this.containerRegistryEnabled = false;
+ this.lfsEnabled = false;
+ }
+ } else if (oldValue === 0) {
+ this.mergeRequestsAccessLevel = value;
+ this.buildsAccessLevel = value;
+ this.containerRegistryEnabled = true;
+ this.lfsEnabled = true;
+ }
+ },
+
+ issuesAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.issues-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.issues-feature', false);
+ },
+
+ mergeRequestsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.merge-requests-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.merge-requests-feature', false);
+ },
+
+ buildsAccessLevel(value, oldValue) {
+ if (value === 0) toggleHiddenClassBySelector('.builds-feature', true);
+ else if (oldValue === 0) toggleHiddenClassBySelector('.builds-feature', false);
+ },
+ },
+};
+
+</script>
+
+<template>
+ <div>
+ <div class="project-visibility-setting">
+ <project-setting-row
+ label="Project visibility"
+ :help-path="visibilityHelpPath"
+ >
+ <div class="project-feature-controls">
+ <div class="select-wrapper">
+ <select
+ name="project[visibility_level]"
+ v-model="visibilityLevel"
+ class="form-control select-control"
+ :disabled="!canChangeVisibilityLevel"
+ >
+ <option
+ :value="visibilityOptions.PRIVATE"
+ :disabled="!visibilityAllowed(visibilityOptions.PRIVATE)"
+ >
+ Private
+ </option>
+ <option
+ :value="visibilityOptions.INTERNAL"
+ :disabled="!visibilityAllowed(visibilityOptions.INTERNAL)"
+ >
+ Internal
+ </option>
+ <option
+ :value="visibilityOptions.PUBLIC"
+ :disabled="!visibilityAllowed(visibilityOptions.PUBLIC)"
+ >
+ Public
+ </option>
+ </select>
+ <i aria-hidden="true" data-hidden="true" class="fa fa-chevron-down"></i>
+ </div>
+ </div>
+ <span class="help-block">{{ visibilityLevelDescription }}</span>
+ <label v-if="visibilityLevel !== visibilityOptions.PUBLIC" class="request-access">
+ <input
+ type="hidden"
+ name="project[request_access_enabled]"
+ :value="requestAccessEnabled"
+ />
+ <input type="checkbox" v-model="requestAccessEnabled" />
+ Allow users to request access
+ </label>
+ </project-setting-row>
+ </div>
+ <div class="project-feature-settings" :class="{ 'highlight-changes': highlightChangesClass }">
+ <project-setting-row
+ label="Issues"
+ help-text="Lightweight issue tracking system for this project"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][issues_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="issuesAccessLevel"
+ />
+ </project-setting-row>
+ <project-setting-row
+ label="Repository"
+ help-text="View and edit files in this project"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][repository_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="repositoryAccessLevel"
+ />
+ </project-setting-row>
+ <div class="project-feature-setting-group">
+ <project-setting-row
+ label="Merge requests"
+ help-text="Submit changes to be merged upstream"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][merge_requests_access_level]"
+ :options="repoFeatureAccessLevelOptions"
+ v-model="mergeRequestsAccessLevel"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ <project-setting-row
+ label="Pipelines"
+ help-text="Build, test, and deploy your changes"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][builds_access_level]"
+ :options="repoFeatureAccessLevelOptions"
+ v-model="buildsAccessLevel"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ <project-setting-row
+ v-if="registryAvailable"
+ label="Container registry"
+ :help-path="registryHelpPath"
+ help-text="Every project can have its own space to store its Docker images"
+ >
+ <project-feature-toggle
+ name="project[container_registry_enabled]"
+ v-model="containerRegistryEnabled"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ <project-setting-row
+ v-if="lfsAvailable"
+ label="Git Large File Storage"
+ :help-path="lfsHelpPath"
+ help-text="Manages large files such as audio, video, and graphics files"
+ >
+ <project-feature-toggle
+ name="project[lfs_enabled]"
+ v-model="lfsEnabled"
+ :disabledInput="!repositoryEnabled"
+ />
+ </project-setting-row>
+ </div>
+ <project-setting-row
+ label="Wiki"
+ help-text="Pages for project documentation"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][wiki_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="wikiAccessLevel"
+ />
+ </project-setting-row>
+ <project-setting-row
+ label="Snippets"
+ help-text="Share code pastes with others out of Git repository"
+ >
+ <project-feature-setting
+ name="project[project_feature_attributes][snippets_access_level]"
+ :options="featureAccessLevelOptions"
+ v-model="snippetsAccessLevel"
+ />
+ </project-setting-row>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/projects/permissions/constants.js b/app/assets/javascripts/projects/permissions/constants.js
new file mode 100644
index 00000000000..ce47562f259
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/constants.js
@@ -0,0 +1,11 @@
+export const visibilityOptions = {
+ PRIVATE: 0,
+ INTERNAL: 10,
+ PUBLIC: 20,
+};
+
+export const visibilityLevelDescriptions = {
+ [visibilityOptions.PRIVATE]: 'The project is accessible only by members of the project. Access must be granted explicitly to each user.',
+ [visibilityOptions.INTERNAL]: 'The project can be accessed by any user who is logged in.',
+ [visibilityOptions.PUBLIC]: 'The project can be accessed by anyone, regardless of authentication.',
+};
diff --git a/app/assets/javascripts/projects/permissions/external.js b/app/assets/javascripts/projects/permissions/external.js
new file mode 100644
index 00000000000..460af4a2111
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/external.js
@@ -0,0 +1,18 @@
+const selectorCache = [];
+
+// workaround since we don't have a polyfill for classList.toggle 2nd parameter
+export function toggleHiddenClass(element, hidden) {
+ if (hidden) {
+ element.classList.add('hidden');
+ } else {
+ element.classList.remove('hidden');
+ }
+}
+
+// hide external feature-specific settings when a given feature is disabled
+export function toggleHiddenClassBySelector(selector, hidden) {
+ if (!selectorCache[selector]) {
+ selectorCache[selector] = document.querySelectorAll(selector);
+ }
+ selectorCache[selector].forEach(elm => toggleHiddenClass(elm, hidden));
+}
diff --git a/app/assets/javascripts/projects/permissions/index.js b/app/assets/javascripts/projects/permissions/index.js
new file mode 100644
index 00000000000..dbde8dda634
--- /dev/null
+++ b/app/assets/javascripts/projects/permissions/index.js
@@ -0,0 +1,13 @@
+import Vue from 'vue';
+import settingsPanel from './components/settings_panel.vue';
+
+export default function initProjectPermissionsSettings() {
+ const mountPoint = document.querySelector('.js-project-permissions-form');
+ const componentPropsEl = document.querySelector('.js-project-permissions-form-data');
+ const componentProps = JSON.parse(componentPropsEl.innerHTML);
+
+ return new Vue({
+ el: mountPoint,
+ render: createElement => createElement(settingsPanel, { props: { ...componentProps } }),
+ });
+}
diff --git a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
index fa5efef2919..8d0c29177e6 100644
--- a/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
+++ b/app/assets/javascripts/projects_dropdown/components/projects_list_search.vue
@@ -27,7 +27,7 @@ export default {
listEmptyMessage() {
return this.searchFailed ?
s__('ProjectsDropdown|Something went wrong on our end.') :
- s__('ProjectsDropdown|No projects matched your query');
+ s__('ProjectsDropdown|Sorry, no projects matched your search');
},
},
};
diff --git a/app/assets/javascripts/projects_dropdown/components/search.vue b/app/assets/javascripts/projects_dropdown/components/search.vue
index b71997234e5..53bc76d0f2d 100644
--- a/app/assets/javascripts/projects_dropdown/components/search.vue
+++ b/app/assets/javascripts/projects_dropdown/components/search.vue
@@ -53,7 +53,7 @@ export default {
class="form-control"
ref="search"
v-model="searchQuery"
- :placeholder="s__('ProjectsDropdown|Search projects')"
+ :placeholder="s__('ProjectsDropdown|Search your projects')"
/>
<i
v-if="!searchQuery"
diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js
index 7fa5996d600..8635ccece6e 100644
--- a/app/assets/javascripts/settings_panels.js
+++ b/app/assets/javascripts/settings_panels.js
@@ -41,4 +41,8 @@ export default function initSettingsPanels() {
$section.on('click.toggleSection', '.js-settings-toggle', () => toggleSection($section));
$section.find('.settings-content:not(.expanded)').on('scroll.expandSection', () => expandSection($section));
});
+
+ if (location.hash) {
+ expandSection($(location.hash));
+ }
}
diff --git a/app/assets/javascripts/user_callout.js b/app/assets/javascripts/user_callout.js
index ff2208baeab..a45b22f3084 100644
--- a/app/assets/javascripts/user_callout.js
+++ b/app/assets/javascripts/user_callout.js
@@ -1,7 +1,11 @@
import Cookies from 'js-cookie';
export default class UserCallout {
- constructor(className = 'user-callout') {
+ constructor(options = {}) {
+ this.options = options;
+
+ const className = this.options.className || 'user-callout';
+
this.userCalloutBody = $(`.${className}`);
this.cookieName = this.userCalloutBody.data('uid');
this.isCalloutDismissed = Cookies.get(this.cookieName);
@@ -17,7 +21,11 @@ export default class UserCallout {
dismissCallout(e) {
const $currentTarget = $(e.currentTarget);
- Cookies.set(this.cookieName, 'true', { expires: 365 });
+ if (this.options.setCalloutPerProject) {
+ Cookies.set(this.cookieName, 'true', { expires: 365, path: this.userCalloutBody.data('project-path') });
+ } else {
+ Cookies.set(this.cookieName, 'true', { expires: 365 });
+ }
if ($currentTarget.hasClass('close')) {
this.userCalloutBody.remove();
diff --git a/app/assets/javascripts/users_select.js b/app/assets/javascripts/users_select.js
index a31fedee021..73676bd6de7 100644
--- a/app/assets/javascripts/users_select.js
+++ b/app/assets/javascripts/users_select.js
@@ -75,7 +75,7 @@ function UsersSelect(currentUser, els) {
if (currentUserInfo) {
input.value = currentUserInfo.id;
- input.dataset.meta = currentUserInfo.name;
+ input.dataset.meta = _.escape(currentUserInfo.name);
} else if (_this.currentUser) {
input.value = _this.currentUser.id;
}
@@ -198,7 +198,7 @@ function UsersSelect(currentUser, els) {
};
}
$value.html(assigneeTemplate(user));
- $collapsedSidebar.attr('title', user.name).tooltip('fixTitle');
+ $collapsedSidebar.attr('title', _.escape(user.name)).tooltip('fixTitle');
return $collapsedSidebar.html(collapsedAssigneeTemplate(user));
});
};
@@ -506,7 +506,7 @@ function UsersSelect(currentUser, els) {
img = "";
if (user.beforeDivider != null) {
- `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${user.name}</a></li>`;
+ `<li><a href='#' class='${selected === true ? 'is-active' : ''}'>${_.escape(user.name)}</a></li>`;
} else {
if (avatar) {
img = "<img src='" + avatar + "' class='avatar avatar-inline' width='32' />";
@@ -518,7 +518,7 @@ function UsersSelect(currentUser, els) {
<a href='#' class='dropdown-menu-user-link ${selected === true ? 'is-active' : ''}'>
${img}
<strong class='dropdown-menu-user-full-name'>
- ${user.name}
+ ${_.escape(user.name)}
</strong>
${username ? `<span class='dropdown-menu-user-username'>${username}</span>` : ''}
</a>
@@ -643,11 +643,11 @@ UsersSelect.prototype.formatResult = function(user) {
} else {
avatar = gon.default_avatar_url;
}
- return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + user.name + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>";
+ return "<div class='user-result " + (!user.username ? 'no-username' : void 0) + "'> <div class='user-image'><img class='avatar avatar-inline s32' src='" + avatar + "'></div> <div class='user-name dropdown-menu-user-full-name'>" + _.escape(user.name) + "</div> <div class='user-username dropdown-menu-user-username'>" + (!user.invite ? "@" + _.escape(user.username) : "") + "</div> </div>";
};
UsersSelect.prototype.formatSelection = function(user) {
- return user.name;
+ return _.escape(user.name);
};
UsersSelect.prototype.user = function(user_id, callback) {
diff --git a/app/assets/javascripts/vue_shared/directives/popover.js b/app/assets/javascripts/vue_shared/directives/popover.js
new file mode 100644
index 00000000000..05fa563cbd0
--- /dev/null
+++ b/app/assets/javascripts/vue_shared/directives/popover.js
@@ -0,0 +1,20 @@
+/**
+ * Helper to user bootstrap popover in vue.js.
+ * Follow docs for html attributes: https://getbootstrap.com/docs/3.3/javascript/#static-popover
+ *
+ * @example
+ * import popover from 'vue_shared/directives/popover.js';
+ * {
+ * directives: [popover]
+ * }
+ * <a v-popover="{options}">popover</a>
+ */
+export default {
+ bind(el, binding) {
+ $(el).popover(binding.value);
+ },
+
+ unbind(el) {
+ $(el).popover('destroy');
+ },
+};
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index c0524bf6aa3..35e7a10379f 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -19,6 +19,7 @@
@import "framework/flash";
@import "framework/forms";
@import "framework/gfm";
+@import "framework/gitlab-theme";
@import "framework/header";
@import "framework/highlight";
@import "framework/issue_box";
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index cf557d94bdd..2bcd23a15e6 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -734,6 +734,11 @@
overflow: hidden;
}
+@mixin dropdown-item-hover {
+ background-color: $dropdown-item-hover-bg;
+ color: $gl-text-color;
+}
+
// TODO: change global style and remove mixin
@mixin new-style-dropdown($selector: '') {
#{$selector}.dropdown-menu,
@@ -760,6 +765,10 @@
padding: 8px 16px;
}
+ &.droplab-item-active button {
+ @include dropdown-item-hover;
+ }
+
a,
button,
.menu-item {
@@ -779,6 +788,8 @@
&:hover,
&:active,
&:focus {
+ @include dropdown-item-hover;
+
background-color: $dropdown-item-hover-bg;
color: $gl-text-color;
@@ -837,17 +848,30 @@
}
}
+@media (max-width: $screen-xs-max) {
+ .navbar-gitlab {
+ li.header-projects,
+ li.header-more,
+ li.header-new,
+ li.header-user {
+ position: static;
+ }
+ }
+
+ header.navbar-gitlab .dropdown {
+ .dropdown-menu,
+ .dropdown-menu-nav {
+ width: 100%;
+ min-width: 100%;
+ }
+ }
+}
+
@include new-style-dropdown('.breadcrumbs-list .dropdown ');
@include new-style-dropdown('.js-namespace-select + ');
header.navbar-gitlab-new .header-content .dropdown-menu.projects-dropdown-menu {
padding: 0;
-
- @media (max-width: $screen-xs-max) {
- display: table;
- left: -50px;
- min-width: 300px;
- }
}
.projects-dropdown-container {
diff --git a/app/assets/stylesheets/framework/gitlab-theme.scss b/app/assets/stylesheets/framework/gitlab-theme.scss
new file mode 100644
index 00000000000..71f764923ff
--- /dev/null
+++ b/app/assets/stylesheets/framework/gitlab-theme.scss
@@ -0,0 +1,265 @@
+/**
+ * Styles the GitLab application with a specific color theme
+ */
+
+@mixin gitlab-theme($color-100, $color-200, $color-500, $color-700, $color-800, $color-900, $color-alternate) {
+ // Header
+
+ header.navbar-gitlab-new {
+ background: linear-gradient(to right, $color-900, $color-800);
+
+ .navbar-collapse {
+ color: $color-200;
+ }
+
+ .container-fluid {
+ .navbar-toggle {
+ border-left: 1px solid lighten($color-700, 10%);
+ }
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ background-color: rgba($color-200, .2);
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $color-900;
+ background-color: $color-alternate;
+
+ svg {
+ fill: currentColor;
+ }
+ }
+
+ &.line-separator {
+ border-left: 1px solid rgba($color-200, .2);
+ }
+ }
+ }
+
+ .navbar-sub-nav {
+ color: $color-200;
+ }
+
+ .nav {
+ > li {
+ color: $color-200;
+
+ > a {
+ svg {
+ fill: $color-200;
+ }
+
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $color-200;
+ }
+ }
+
+ &:hover,
+ &:focus {
+ @media (min-width: $screen-sm-min) {
+ background-color: rgba($color-200, .2);
+ }
+
+ svg {
+ fill: currentColor;
+ }
+ }
+ }
+
+ &.active > a,
+ &.dropdown.open > a {
+ color: $color-900;
+ background-color: $color-alternate;
+
+ &:hover {
+ svg {
+ fill: $color-900;
+ }
+ }
+ }
+
+ .impersonated-user,
+ .impersonated-user:hover {
+ svg {
+ fill: $color-900;
+ }
+ }
+ }
+ }
+ }
+
+ .title {
+ > a {
+ &:hover,
+ &:focus {
+ background-color: rgba($color-200, .2);
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: rgba($color-200, .2);
+
+ &:hover {
+ background-color: rgba($color-200, .3);
+ }
+ }
+
+ .location-badge {
+ color: $color-100;
+ background-color: rgba($color-200, .1);
+ border-right: 1px solid $color-800;
+ }
+
+ .search-input::placeholder {
+ color: rgba($color-200, .8);
+ }
+
+ .search-input-wrap {
+ .search-icon,
+ .clear-icon {
+ color: rgba($color-200, .8);
+ }
+ }
+
+ &.search-active {
+ form {
+ background-color: $white-light;
+ }
+
+ .location-badge {
+ color: $gl-text-color;
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ color: rgba($color-200, .8);
+ }
+ }
+ }
+ }
+
+ .btn-sign-in {
+ background-color: $color-100;
+ color: $color-900;
+ }
+
+
+ // Sidebar
+ .nav-sidebar li.active {
+ box-shadow: inset 4px 0 0 $color-700;
+
+ > a {
+ color: $color-900;
+ }
+
+ svg {
+ fill: $color-900;
+ }
+ }
+}
+
+
+body {
+ &.ui_indigo {
+ @include gitlab-theme($indigo-100, $indigo-200, $indigo-500, $indigo-700, $indigo-800, $indigo-900, $white-light);
+ }
+
+ &.ui_dark {
+ @include gitlab-theme($theme-gray-100, $theme-gray-200, $theme-gray-500, $theme-gray-700, $theme-gray-800, $theme-gray-900, $white-light);
+ }
+
+ &.ui_blue {
+ @include gitlab-theme($theme-blue-100, $theme-blue-200, $theme-blue-500, $theme-blue-700, $theme-blue-800, $theme-blue-900, $white-light);
+ }
+
+ &.ui_green {
+ @include gitlab-theme($theme-green-100, $theme-green-200, $theme-green-500, $theme-green-700, $theme-green-800, $theme-green-900, $white-light);
+ }
+
+ &.ui_light {
+ @include gitlab-theme($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-700, $theme-gray-700, $theme-gray-100, $theme-gray-700);
+
+ header.navbar-gitlab-new {
+ background: $theme-gray-100;
+ box-shadow: 0 2px 0 0 $border-color;
+
+ .logo-text svg {
+ fill: $theme-gray-900;
+ }
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ > li {
+ > a:hover,
+ > a:focus {
+ color: $theme-gray-900;
+ }
+
+ &.active > a {
+ color: $white-light;
+
+ &:hover {
+ color: $white-light;
+ }
+ }
+ }
+ }
+
+ .container-fluid {
+ .navbar-toggle,
+ .navbar-toggle:hover {
+ color: $theme-gray-700;
+ border-left: 1px solid $theme-gray-200;
+ }
+ }
+ }
+
+ .search {
+ form {
+ background-color: $white-light;
+ box-shadow: inset 0 0 0 1px $border-color;
+
+ &:hover {
+ background-color: $white-light;
+ box-shadow: inset 0 0 0 1px $blue-100;
+
+ .location-badge {
+ box-shadow: inset 0 0 0 1px $blue-100;
+ }
+ }
+ }
+
+ .search-input-wrap {
+ .search-icon {
+ color: $theme-gray-200;
+ }
+ }
+
+ .location-badge {
+ color: $theme-gray-700;
+ box-shadow: inset 0 0 0 1px $border-color;
+ background-color: $nav-badge-bg;
+ border-right: 0;
+ }
+ }
+
+ .nav-sidebar li.active {
+ > a {
+ color: $theme-gray-900;
+ }
+
+ svg {
+ fill: $theme-gray-900;
+ }
+ }
+ }
+}
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index b00a2d053e2..ab3c34df1fb 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -111,7 +111,6 @@ header {
svg {
height: 16px;
width: 23px;
- fill: currentColor;
}
}
diff --git a/app/assets/stylesheets/framework/nav.scss b/app/assets/stylesheets/framework/nav.scss
index e20108b171b..5ffa67a1220 100644
--- a/app/assets/stylesheets/framework/nav.scss
+++ b/app/assets/stylesheets/framework/nav.scss
@@ -288,11 +288,7 @@
display: flex;
max-width: 350px;
overflow: hidden;
-
- @media(max-width: $screen-xs-max) {
- width: 100%;
- max-width: none;
- }
+ float: right;
.new-project-item-link {
white-space: nowrap;
@@ -305,6 +301,23 @@
}
}
+.empty-state .project-item-select-holder.btn-group {
+ float: none;
+ display: inline-block;
+
+ .btn {
+ // overrides styles applied to plain `.empty-state .btn`
+ margin: 10px 0;
+ max-width: 300px;
+ width: auto;
+
+ @media(max-width: $screen-xs-max) {
+ max-width: 250px;
+ }
+
+ }
+}
+
.new-project-item-select-button .fa-caret-down {
margin-left: 2px;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 88b08998dfd..3857226cddb 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -74,6 +74,8 @@ $red-700: #a62d19;
$red-800: #8b2615;
$red-900: #711e11;
+// GitLab themes
+
$indigo-50: #f7f7ff;
$indigo-100: #ebebfa;
$indigo-200: #d1d1f0;
@@ -86,6 +88,43 @@ $indigo-800: #393982;
$indigo-900: #292961;
$indigo-950: #1a1a40;
+$theme-gray-50: #fafafa;
+$theme-gray-100: #f2f2f2;
+$theme-gray-200: #dfdfdf;
+$theme-gray-300: #cccccc;
+$theme-gray-400: #bababa;
+$theme-gray-500: #a7a7a7;
+$theme-gray-600: #949494;
+$theme-gray-700: #707070;
+$theme-gray-800: #4f4f4f;
+$theme-gray-900: #2e2e2e;
+$theme-gray-950: #1f1f1f;
+
+$theme-blue-50: #f4f8fc;
+$theme-blue-100: #e6edf5;
+$theme-blue-200: #c8d7e6;
+$theme-blue-300: #97b3cf;
+$theme-blue-400: #648cb4;
+$theme-blue-500: #4a79a8;
+$theme-blue-600: #3e6fa0;
+$theme-blue-700: #305c88;
+$theme-blue-800: #25496e;
+$theme-blue-900: #1a3652;
+$theme-blue-950: #0f2235;
+
+$theme-green-50: #f2faf6;
+$theme-green-100: #e4f3ea;
+$theme-green-200: #c0dfcd;
+$theme-green-300: #8ac2a1;
+$theme-green-400: #52a274;
+$theme-green-500: #35935c;
+$theme-green-600: #288a50;
+$theme-green-700: #1c7441;
+$theme-green-800: #145d33;
+$theme-green-900: #0d4524;
+$theme-green-950: #072d16;
+
+
$black: #000;
$black-transparent: rgba(0, 0, 0, 0.3);
$almost-black: #242424;
@@ -540,6 +579,11 @@ $project-breadcrumb-color: #999;
$project-private-forks-notice-odd: $green-600;
$project-network-controls-color: #888;
+$feature-toggle-color: #fff;
+$feature-toggle-text-color: #fff;
+$feature-toggle-color-disabled: #999;
+$feature-toggle-color-enabled: #4a8bee;
+
/*
* Runners
*/
diff --git a/app/assets/stylesheets/new_nav.scss b/app/assets/stylesheets/new_nav.scss
index 2b6c0fc015c..8e095cbdd7e 100644
--- a/app/assets/stylesheets/new_nav.scss
+++ b/app/assets/stylesheets/new_nav.scss
@@ -9,10 +9,20 @@
header.navbar-gitlab-new {
color: $white-light;
- background: linear-gradient(to right, $indigo-900, $indigo-800);
border-bottom: 0;
min-height: $new-navbar-height;
+ .logo-text {
+ line-height: initial;
+
+ svg {
+ width: 55px;
+ height: 14px;
+ margin: 0;
+ fill: $white-light;
+ }
+ }
+
.header-content {
display: -webkit-flex;
display: flex;
@@ -38,10 +48,10 @@ header.navbar-gitlab-new {
img {
height: 28px;
- margin-right: 10px;
+ margin-right: 8px;
}
- > a {
+ a {
display: -webkit-flex;
display: flex;
align-items: center;
@@ -54,22 +64,6 @@ header.navbar-gitlab-new {
margin-right: 8px;
}
}
-
- .logo-text {
- line-height: initial;
-
- svg {
- width: 55px;
- height: 14px;
- margin: 0;
- fill: $white-light;
- }
- }
-
- &:hover,
- &:focus {
- background-color: rgba($indigo-200, .2);
- }
}
}
@@ -106,7 +100,6 @@ header.navbar-gitlab-new {
.navbar-collapse {
padding-left: 0;
- color: $indigo-200;
box-shadow: 0;
@media (max-width: $screen-xs-max) {
@@ -132,7 +125,6 @@ header.navbar-gitlab-new {
font-size: 14px;
text-align: center;
color: currentColor;
- border-left: 1px solid lighten($indigo-700, 10%);
&:hover,
&:focus,
@@ -167,63 +159,49 @@ header.navbar-gitlab-new {
will-change: color;
margin: 4px 2px;
padding: 6px 8px;
- color: $indigo-200;
height: 32px;
@media (max-width: $screen-xs-max) {
padding: 0;
}
- svg {
- fill: $indigo-200;
- }
-
&.header-user-dropdown-toggle {
margin-left: 2px;
.header-user-avatar {
- border-color: $indigo-200;
margin-right: 0;
}
}
- }
-
- .header-new-dropdown-toggle {
- margin-right: 0;
- }
- > a:hover,
- > a:focus {
- text-decoration: none;
- outline: 0;
- opacity: 1;
- color: $white-light;
-
- @media (min-width: $screen-sm-min) {
- background-color: rgba($indigo-200, .2);
- }
+ &:hover,
+ &:focus {
+ text-decoration: none;
+ outline: 0;
+ opacity: 1;
+ color: $white-light;
- svg {
- fill: currentColor;
- }
+ svg {
+ fill: currentColor;
+ }
- &.header-user-dropdown-toggle {
- .header-user-avatar {
- border-color: $white-light;
+ &.header-user-dropdown-toggle {
+ .header-user-avatar {
+ border-color: $white-light;
+ }
}
}
}
+ .header-new-dropdown-toggle {
+ margin-right: 0;
+ }
+
.impersonated-user,
.impersonated-user:hover {
margin-right: 1px;
background-color: $white-light;
border-top-right-radius: 0;
border-bottom-right-radius: 0;
-
- svg {
- fill: $indigo-900;
- }
}
.impersonation-btn,
@@ -241,8 +219,6 @@ header.navbar-gitlab-new {
&.active > a,
&.dropdown.open > a {
- color: $indigo-900;
- background-color: $white-light;
svg {
fill: currentColor;
@@ -256,7 +232,6 @@ header.navbar-gitlab-new {
display: -webkit-flex;
display: flex;
margin: 0 0 0 6px;
- color: $indigo-200;
.dropdown-chevron {
position: relative;
@@ -274,17 +249,6 @@ header.navbar-gitlab-new {
text-decoration: none;
outline: 0;
color: $white-light;
- background-color: rgba($indigo-200, .2);
-
- svg {
- fill: currentColor;
- }
- }
-
- &.active > a,
- &.dropdown.open > a {
- color: $indigo-900;
- background-color: $white-light;
svg {
fill: currentColor;
@@ -309,7 +273,6 @@ header.navbar-gitlab-new {
}
&.line-separator {
- border-left: 1px solid rgba($indigo-200, .2);
margin: 8px;
}
}
@@ -339,17 +302,14 @@ header.navbar-gitlab-new {
height: 32px;
border: 0;
border-radius: $border-radius-default;
- background-color: rgba($indigo-200, .2);
transition: border-color ease-in-out 0.15s, background-color ease-in-out 0.15s;
&:hover {
- background-color: rgba($indigo-200, .3);
box-shadow: none;
}
}
&.search-active form {
- background-color: $white-light;
box-shadow: none;
.search-input {
@@ -377,43 +337,26 @@ header.navbar-gitlab-new {
}
.search-input::placeholder {
- color: rgba($indigo-200, .8);
transition: color ease-in-out 0.15s;
}
.location-badge {
font-size: 12px;
- color: $indigo-100;
- background-color: rgba($indigo-200, .1);
- will-change: color;
margin: -4px 4px -4px -4px;
line-height: 25px;
padding: 4px 8px;
border-radius: 2px 0 0 2px;
- border-right: 1px solid $indigo-800;
height: 32px;
transition: border-color ease-in-out 0.15s;
}
- .search-input-wrap {
- .search-icon,
- .clear-icon {
- color: rgba($indigo-200, .8);
- }
- }
-
&.search-active {
.location-badge {
- color: $gl-text-color;
background-color: $nav-badge-bg;
border-color: $border-color;
}
.search-input-wrap {
- .search-icon {
- color: rgba($indigo-200, .8);
- }
-
.clear-icon {
color: $white-light;
}
@@ -517,8 +460,6 @@ header.navbar-gitlab-new {
.btn-sign-in {
margin-top: 3px;
- background-color: $indigo-100;
- color: $indigo-900;
font-weight: $gl-font-weight-bold;
&:hover {
diff --git a/app/assets/stylesheets/new_sidebar.scss b/app/assets/stylesheets/new_sidebar.scss
index 3082f728ac8..4bbd30056a9 100644
--- a/app/assets/stylesheets/new_sidebar.scss
+++ b/app/assets/stylesheets/new_sidebar.scss
@@ -155,16 +155,9 @@ $new-sidebar-collapsed-width: 50px;
}
li.active {
- box-shadow: inset 4px 0 0 $active-border;
-
> a {
- color: $active-color;
font-weight: $gl-font-weight-bold;
}
-
- svg {
- fill: $active-color;
- }
}
@media (max-width: $screen-xs-max) {
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index 587a202d6dd..994707422bb 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -226,6 +226,14 @@
vertical-align: baseline;
}
+ a.autodevops-badge {
+ color: $white-light;
+ }
+
+ a.autodevops-link {
+ color: $gl-link-color;
+ }
+
.commit-row-description {
font-size: 14px;
padding: 10px 15px;
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index cb8815e4775..296b6310552 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -202,6 +202,10 @@
.btn-group.open .dropdown-toggle {
box-shadow: none;
}
+
+ .pipeline-tags .label-container {
+ white-space: normal;
+ }
}
.stage-cell {
@@ -932,3 +936,8 @@ button.mini-pipeline-graph-dropdown-toggle {
.pipelines-container .top-area .nav-controls > .btn:last-child {
float: none;
}
+
+.autodevops-title {
+ font-weight: $gl-font-weight-normal;
+ line-height: 1.5;
+}
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 305feaacaa1..c197494b152 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,3 +1,67 @@
+@mixin application-theme-preview($color-1, $color-2, $color-3, $color-4) {
+ .one {
+ background-color: $color-1;
+ border-top-left-radius: $border-radius-default;
+ }
+
+ .two {
+ background-color: $color-2;
+ border-top-right-radius: $border-radius-default;
+ }
+
+ .three {
+ background-color: $color-3;
+ border-bottom-left-radius: $border-radius-default;
+ }
+
+ .four {
+ background-color: $color-4;
+ border-bottom-right-radius: $border-radius-default;
+ }
+}
+
+.application-theme {
+ label {
+ margin-right: 20px;
+ text-align: center;
+ }
+
+ .preview {
+ font-size: 0;
+ margin-bottom: 10px;
+
+ &.indigo {
+ @include application-theme-preview($indigo-900, $indigo-700, $indigo-800, $indigo-500);
+ }
+
+ &.dark {
+ @include application-theme-preview($theme-gray-900, $theme-gray-700, $theme-gray-800, $theme-gray-600);
+ }
+
+ &.light {
+ @include application-theme-preview($theme-gray-600, $theme-gray-200, $theme-gray-400, $theme-gray-100);
+ }
+
+ &.blue {
+ @include application-theme-preview($theme-blue-900, $theme-blue-700, $theme-blue-800, $theme-blue-500);
+ }
+
+ &.green {
+ @include application-theme-preview($theme-green-900, $theme-green-700, $theme-green-800, $theme-green-500);
+ }
+ }
+
+ .preview-row {
+ display: block;
+ }
+
+ .quadrant {
+ display: inline-block;
+ height: 50px;
+ width: 80px;
+ }
+}
+
.syntax-theme {
label {
margin-right: 20px;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index dd600a27545..94e4f4334d4 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -10,41 +10,6 @@
.edit-project,
.import-project {
- .sharing-and-permissions {
- .header {
- padding-top: $gl-vert-padding;
- }
-
- .label-light {
- margin-bottom: 0;
- }
-
- .help-block {
- margin-top: 0;
- }
-
- .form-group {
- margin-bottom: 5px;
- }
-
- > .form-group {
- padding-left: 0;
- }
-
- select option[disabled] {
- display: none;
- }
- }
-
- select {
- transition: background 2s ease-out;
-
- &.highlight-changes {
- background: $highlight-changes-color;
- transition: none;
- }
- }
-
.help-block {
margin-bottom: 10px;
}
@@ -90,6 +55,162 @@
}
}
+.toggle-wrapper {
+ margin-top: 5px;
+}
+
+.project-feature-row > .toggle-wrapper {
+ margin: 10px 0;
+}
+
+.project-visibility-setting,
+.project-feature-settings {
+ border: 1px solid $border-color;
+ padding: 10px 32px;
+
+ @media (max-width: $screen-xs-min) {
+ padding: 10px 20px;
+ }
+}
+
+.project-visibility-setting .request-access {
+ line-height: 2;
+}
+
+.project-feature-settings {
+ background: $gray-lighter;
+ border-top: none;
+ margin-bottom: 16px;
+}
+
+.project-repo-select {
+ transition: background 2s ease-out;
+
+ &:disabled {
+ opacity: 0.75;
+ }
+
+ .highlight-changes & {
+ background: $highlight-changes-color;
+ transition: none;
+ }
+}
+
+.project-feature-controls {
+ display: flex;
+ align-items: center;
+ margin: 8px 0;
+ max-width: 432px;
+
+ .toggle-wrapper {
+ flex: 0;
+ margin-right: 10px;
+ }
+
+ .select-wrapper {
+ flex: 1;
+ }
+}
+
+.project-feature-setting-group {
+ padding-left: 32px;
+
+ .project-feature-controls {
+ max-width: 400px;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ padding-left: 20px;
+ }
+}
+
+.project-feature-toggle {
+ position: relative;
+ border: none;
+ outline: 0;
+ display: block;
+ width: 100px;
+ height: 24px;
+ cursor: pointer;
+ user-select: none;
+ background: $feature-toggle-color-disabled;
+ border-radius: 12px;
+ padding: 3px;
+ transition: all .4s ease;
+
+ &::selection,
+ &::before::selection,
+ &::after::selection {
+ background: none;
+ }
+
+ &::before {
+ color: $feature-toggle-text-color;
+ font-size: 12px;
+ line-height: 24px;
+ position: absolute;
+ top: 0;
+ left: 25px;
+ right: 5px;
+ text-align: center;
+ overflow: hidden;
+ text-overflow: ellipsis;
+ animation: animate-disabled .2s ease-in;
+ content: attr(data-disabled-text);
+ }
+
+ &::after {
+ position: relative;
+ display: block;
+ content: "";
+ width: 22px;
+ height: 18px;
+ left: 0;
+ border-radius: 9px;
+ background: $feature-toggle-color;
+ transition: all .2s ease;
+ }
+
+ &.checked {
+ background: $feature-toggle-color-enabled;
+
+ &::before {
+ left: 5px;
+ right: 25px;
+ animation: animate-enabled .2s ease-in;
+ content: attr(data-enabled-text);
+ }
+
+ &::after {
+ left: calc(100% - 22px);
+ }
+ }
+
+ &.disabled {
+ opacity: 0.4;
+ cursor: not-allowed;
+ }
+
+ @media (max-width: $screen-xs-min) {
+ width: 50px;
+
+ &::before,
+ &.checked::before {
+ display: none;
+ }
+ }
+
+ @keyframes animate-enabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+
+ @keyframes animate-disabled {
+ 0%, 35% { opacity: 0; }
+ 100% { opacity: 1; }
+ }
+}
+
.project-home-panel,
.group-home-panel {
padding-top: 24px;
diff --git a/app/assets/stylesheets/pages/repo.scss b/app/assets/stylesheets/pages/repo.scss
index efc47861768..69abb13add4 100644
--- a/app/assets/stylesheets/pages/repo.scss
+++ b/app/assets/stylesheets/pages/repo.scss
@@ -43,8 +43,10 @@
display: inline-block;
}
-.blob-viewer[data-type="rich"] {
- margin: 20px;
+@media (min-width: $screen-md-min) {
+ .blob-viewer[data-type="rich"] {
+ margin: 20px;
+ }
}
.repository-view {
diff --git a/app/controllers/admin/broadcast_messages_controller.rb b/app/controllers/admin/broadcast_messages_controller.rb
index 762e36ee2e9..c49b6459452 100644
--- a/app/controllers/admin/broadcast_messages_controller.rb
+++ b/app/controllers/admin/broadcast_messages_controller.rb
@@ -2,7 +2,7 @@ class Admin::BroadcastMessagesController < Admin::ApplicationController
before_action :finder, only: [:edit, :update, :destroy]
def index
- @broadcast_messages = BroadcastMessage.reorder("ends_at DESC").page(params[:page])
+ @broadcast_messages = BroadcastMessage.order(ends_at: :desc).page(params[:page])
@broadcast_message = BroadcastMessage.new
end
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index 05e749c00c0..e85cdcb8db7 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -1,7 +1,7 @@
class Admin::DashboardController < Admin::ApplicationController
def index
- @projects = Project.without_deleted.with_route.limit(10)
- @users = User.limit(10)
- @groups = Group.with_route.limit(10)
+ @projects = Project.order_id_desc.without_deleted.with_route.limit(10)
+ @users = User.order_id_desc.limit(10)
+ @groups = Group.order_id_desc.with_route.limit(10)
end
end
diff --git a/app/controllers/admin/users_controller.rb b/app/controllers/admin/users_controller.rb
index a99563b7100..cbcef70e957 100644
--- a/app/controllers/admin/users_controller.rb
+++ b/app/controllers/admin/users_controller.rb
@@ -17,7 +17,7 @@ class Admin::UsersController < Admin::ApplicationController
end
def keys
- @keys = user.keys
+ @keys = user.keys.order_id_desc
end
def new
@@ -211,6 +211,7 @@ class Admin::UsersController < Admin::ApplicationController
:provider,
:remember_me,
:skype,
+ :theme_id,
:twitter,
:username,
:website_url
diff --git a/app/controllers/dashboard/groups_controller.rb b/app/controllers/dashboard/groups_controller.rb
index 742157d113d..8057a0b455c 100644
--- a/app/controllers/dashboard/groups_controller.rb
+++ b/app/controllers/dashboard/groups_controller.rb
@@ -1,5 +1,7 @@
class Dashboard::GroupsController < Dashboard::ApplicationController
def index
+ @sort = params[:sort] || 'id_desc'
+
@groups =
if params[:parent_id] && Group.supports_nested_groups?
parent = Group.find_by(id: params[:parent_id])
@@ -15,7 +17,7 @@ class Dashboard::GroupsController < Dashboard::ApplicationController
@groups = @groups.search(params[:filter_groups]) if params[:filter_groups].present?
@groups = @groups.includes(:route)
- @groups = @groups.sort(@sort = params[:sort])
+ @groups = @groups.sort(@sort)
@groups = @groups.page(params[:page])
respond_to do |format|
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index 994e736d66e..3769a2cde33 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -10,7 +10,7 @@ class GroupsController < Groups::ApplicationController
# Authorize
before_action :authorize_admin_group!, only: [:edit, :update, :destroy, :projects]
- before_action :authorize_create_group!, only: [:new, :create]
+ before_action :authorize_create_group!, only: [:new]
before_action :group_projects, only: [:projects, :activity, :issues, :merge_requests]
before_action :group_merge_requests, only: [:merge_requests]
@@ -25,14 +25,7 @@ class GroupsController < Groups::ApplicationController
end
def new
- @group = Group.new
-
- if params[:parent_id].present?
- parent = Group.find_by(id: params[:parent_id])
- if can?(current_user, :create_subgroup, parent)
- @group.parent = parent
- end
- end
+ @group = Group.new(params.permit(:parent_id))
end
def create
@@ -128,9 +121,14 @@ class GroupsController < Groups::ApplicationController
end
def authorize_create_group!
- unless can?(current_user, :create_group)
- return render_404
- end
+ allowed = if params[:parent_id].present?
+ parent = Group.find_by(id: params[:parent_id])
+ can?(current_user, :create_subgroup, parent)
+ else
+ can?(current_user, :create_group)
+ end
+
+ render_404 unless allowed
end
def determine_layout
diff --git a/app/controllers/profiles/emails_controller.rb b/app/controllers/profiles/emails_controller.rb
index 17b66df43e7..ddb67d1c4d1 100644
--- a/app/controllers/profiles/emails_controller.rb
+++ b/app/controllers/profiles/emails_controller.rb
@@ -1,7 +1,7 @@
class Profiles::EmailsController < Profiles::ApplicationController
def index
@primary = current_user.email
- @emails = current_user.emails
+ @emails = current_user.emails.order_id_desc
end
def create
diff --git a/app/controllers/profiles/keys_controller.rb b/app/controllers/profiles/keys_controller.rb
index 88f49da555a..f9f0e8eef83 100644
--- a/app/controllers/profiles/keys_controller.rb
+++ b/app/controllers/profiles/keys_controller.rb
@@ -2,7 +2,7 @@ class Profiles::KeysController < Profiles::ApplicationController
skip_before_action :authenticate_user!, only: [:get_keys]
def index
- @keys = current_user.keys
+ @keys = current_user.keys.order_id_desc
@key = Key.new
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 1e557c47638..cce2a847b53 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -35,7 +35,8 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:color_scheme_id,
:layout,
:dashboard,
- :project_view
+ :project_view,
+ :theme_id
)
end
end
diff --git a/app/controllers/projects/merge_requests/diffs_controller.rb b/app/controllers/projects/merge_requests/diffs_controller.rb
index 109418c73f7..d60a24d3f1d 100644
--- a/app/controllers/projects/merge_requests/diffs_controller.rb
+++ b/app/controllers/projects/merge_requests/diffs_controller.rb
@@ -27,7 +27,7 @@ class Projects::MergeRequests::DiffsController < Projects::MergeRequests::Applic
@merge_request.merge_request_diff
end
- @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff
+ @merge_request_diffs = @merge_request.merge_request_diffs.viewable.select_without_diff.order_id_desc
@comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id }
if params[:start_sha].present?
diff --git a/app/controllers/projects/pipelines_settings_controller.rb b/app/controllers/projects/pipelines_settings_controller.rb
index 9d24ebe2138..abab2e2f0c9 100644
--- a/app/controllers/projects/pipelines_settings_controller.rb
+++ b/app/controllers/projects/pipelines_settings_controller.rb
@@ -6,7 +6,7 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
end
def update
- if @project.update_attributes(update_params)
+ if @project.update(update_params)
flash[:notice] = "Pipelines settings for '#{@project.name}' were successfully updated."
redirect_to project_settings_ci_cd_path(@project)
else
@@ -16,14 +16,12 @@ class Projects::PipelinesSettingsController < Projects::ApplicationController
private
- def create_params
- params.require(:pipeline).permit(:ref)
- end
-
def update_params
params.require(:project).permit(
- :runners_token, :builds_enabled, :build_allow_git_fetch, :build_timeout_in_minutes, :build_coverage_regex,
- :public_builds, :auto_cancel_pending_pipelines, :ci_config_path
+ :runners_token, :builds_enabled, :build_allow_git_fetch,
+ :build_timeout_in_minutes, :build_coverage_regex, :public_builds,
+ :auto_cancel_pending_pipelines, :ci_config_path,
+ auto_devops_attributes: [:id, :domain, :enabled]
)
end
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index f8ff7413b53..d925dcd21ff 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -47,6 +47,10 @@ class Projects::ProjectMembersController < Projects::ApplicationController
end
end
+ def import
+ @projects = current_user.authorized_projects.order_id_desc
+ end
+
def apply_import
source_project = Project.find(params[:source_project_id])
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 15a2ff56b92..b029b31f9af 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -8,6 +8,7 @@ module Projects
define_secret_variables
define_triggers_variables
define_badges_variables
+ define_auto_devops_variables
end
private
@@ -42,6 +43,10 @@ module Projects
badge.new(@project, @ref).metadata
end
end
+
+ def define_auto_devops_variables
+ @auto_devops = @project.auto_devops || ProjectAutoDevops.new
+ end
end
end
end
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
index 79eb45568be..038d5565a1e 100644
--- a/app/finders/move_to_project_finder.rb
+++ b/app/finders/move_to_project_finder.rb
@@ -9,6 +9,7 @@ class MoveToProjectFinder
projects = @user.projects_where_can_admin_issues
projects = projects.search(search) if search.present?
projects = projects.excluding_project(from_project)
+ projects = projects.order_id_desc
# infinite scroll using offset
projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index fa6fea2588a..eac6095d8dc 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -121,7 +121,7 @@ class ProjectsFinder < UnionFinder
end
def sort(items)
- params[:sort].present? ? items.sort(params[:sort]) : items
+ params[:sort].present? ? items.sort(params[:sort]) : items.order_id_desc
end
def by_archived(projects)
diff --git a/app/finders/todos_finder.rb b/app/finders/todos_finder.rb
index b276116f0c6..3502bf08971 100644
--- a/app/finders/todos_finder.rb
+++ b/app/finders/todos_finder.rb
@@ -118,7 +118,7 @@ class TodosFinder
end
def sort(items)
- params[:sort] ? items.sort(params[:sort]) : items.reorder(id: :desc)
+ params[:sort] ? items.sort(params[:sort]) : items.order_id_desc
end
def by_action(items)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index b93f5f0af1c..7bd34df5c95 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -115,6 +115,7 @@ module ApplicationSettingsHelper
:after_sign_up_text,
:akismet_api_key,
:akismet_enabled,
+ :auto_devops_enabled,
:clientside_sentry_dsn,
:clientside_sentry_enabled,
:container_registry_token_expire_delay,
diff --git a/app/helpers/auto_devops_helper.rb b/app/helpers/auto_devops_helper.rb
new file mode 100644
index 00000000000..4ff38f86b5f
--- /dev/null
+++ b/app/helpers/auto_devops_helper.rb
@@ -0,0 +1,7 @@
+module AutoDevopsHelper
+ def show_auto_devops_callout?(project)
+ show_callout?('auto_devops_settings_dismissed') &&
+ can?(current_user, :admin_pipeline, project) &&
+ project.has_auto_devops_implicitly_disabled?
+ end
+end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 9651f9733f9..08fb9db6c0f 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -137,7 +137,7 @@ module CommitsHelper
text =
if options[:avatar]
- %Q{<span class="commit-#{options[:source]}-name">#{person_name}</span>}
+ content_tag(:span, person_name, class: "commit-#{options[:source]}-name")
else
person_name
end
@@ -148,9 +148,9 @@ module CommitsHelper
}
if user.nil?
- mail_to(source_email, text.html_safe, options)
+ mail_to(source_email, text, options)
else
- link_to(text.html_safe, user_path(user), options)
+ link_to(text, user_path(user), options)
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index eab1feb8a1f..36b79da1bde 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -3,6 +3,10 @@ module GroupsHelper
can?(current_user, :change_visibility_level, group)
end
+ def can_change_share_with_group_lock?(group)
+ can?(current_user, :change_share_with_group_lock, group)
+ end
+
def group_icon(group)
if group.is_a?(String)
group = Group.find_by_full_path(group)
@@ -65,6 +69,20 @@ module GroupsHelper
{ group_name: group.name }
end
+ def share_with_group_lock_help_text(group)
+ return default_help unless group.parent&.share_with_group_lock?
+
+ if group.share_with_group_lock?
+ if can?(current_user, :change_share_with_group_lock, group.parent)
+ ancestor_locked_but_you_can_override(group)
+ else
+ ancestor_locked_so_ask_the_owner(group)
+ end
+ else
+ ancestor_locked_and_has_been_overridden(group)
+ end
+ end
+
private
def group_title_link(group, hidable: false, show_avatar: false)
@@ -80,4 +98,45 @@ module GroupsHelper
output.html_safe
end
end
+
+ def ancestor_group(group)
+ ancestor = oldest_consecutively_locked_ancestor(group)
+ if can?(current_user, :read_group, ancestor)
+ link_to ancestor.name, group_path(ancestor)
+ else
+ ancestor.name
+ end
+ end
+
+ def remove_the_share_with_group_lock_from_ancestor(group)
+ ancestor = oldest_consecutively_locked_ancestor(group)
+ text = s_("GroupSettings|remove the share with group lock from %{ancestor_group_name}") % { ancestor_group_name: ancestor.name }
+ if can?(current_user, :admin_group, ancestor)
+ link_to text, edit_group_path(ancestor)
+ else
+ text
+ end
+ end
+
+ def oldest_consecutively_locked_ancestor(group)
+ group.ancestors.find do |group|
+ !group.has_parent? || !group.parent.share_with_group_lock?
+ end
+ end
+
+ def default_help
+ s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner.")
+ end
+
+ def ancestor_locked_but_you_can_override(group)
+ s_("GroupSettings|This setting is applied on %{ancestor_group}. You can override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
+ end
+
+ def ancestor_locked_so_ask_the_owner(group)
+ s_("GroupSettings|This setting is applied on %{ancestor_group}. To share projects in this group with another group, ask the owner to override the setting or %{remove_ancestor_share_with_group_lock}.").html_safe % { ancestor_group: ancestor_group(group), remove_ancestor_share_with_group_lock: remove_the_share_with_group_lock_from_ancestor(group) }
+ end
+
+ def ancestor_locked_and_has_been_overridden(group)
+ s_("GroupSettings|This setting is applied on %{ancestor_group} and has been overridden on this subgroup.").html_safe % { ancestor_group: ancestor_group(group) }
+ end
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 3d0fdce6a43..212cdbb8157 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -56,7 +56,7 @@ module IssuesHelper
end
def project_options(issuable, current_user, ability: :read_project)
- projects = current_user.authorized_projects
+ projects = current_user.authorized_projects.order_id_desc
projects = projects.select do |project|
current_user.can?(ability, project)
end
diff --git a/app/helpers/preferences_helper.rb b/app/helpers/preferences_helper.rb
index d36bb4ab074..0d7347ed30d 100644
--- a/app/helpers/preferences_helper.rb
+++ b/app/helpers/preferences_helper.rb
@@ -40,6 +40,10 @@ module PreferencesHelper
]
end
+ def user_application_theme
+ @user_application_theme ||= Gitlab::Themes.for_user(current_user).css_class
+ end
+
def user_color_scheme
Gitlab::ColorSchemes.for_user(current_user).css_class
end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 86665ea2aec..c0114dd0256 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -15,9 +15,13 @@ module ProjectsHelper
end
def link_to_member_avatar(author, opts = {})
- default_opts = { avatar: true, name: true, size: 16, author_class: 'author', title: ":name" }
+ default_opts = { size: 16 }
opts = default_opts.merge(opts)
- image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]}", alt: '') if opts[:avatar]
+
+ classes = %W[avatar avatar-inline s#{opts[:size]}]
+ classes << opts[:avatar_class] if opts[:avatar_class]
+
+ image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: classes, alt: '')
end
def link_to_member(project, author, opts = {}, &block)
@@ -29,7 +33,7 @@ module ProjectsHelper
author_html = ""
# Build avatar image tag
- author_html << image_tag(avatar_icon(author, opts[:size]), width: opts[:size], class: "avatar avatar-inline #{"s#{opts[:size]}" if opts[:size]} #{opts[:avatar_class] if opts[:avatar_class]}", alt: '') if opts[:avatar]
+ author_html << link_to_member_avatar(author, opts) if opts[:avatar]
# Build name span tag
if opts[:by_username]
@@ -541,6 +545,43 @@ module ProjectsHelper
current_application_settings.restricted_visibility_levels || []
end
+ def project_permissions_settings(project)
+ feature = project.project_feature
+ {
+ visibilityLevel: project.visibility_level,
+ requestAccessEnabled: !!project.request_access_enabled,
+ issuesAccessLevel: feature.issues_access_level,
+ repositoryAccessLevel: feature.repository_access_level,
+ mergeRequestsAccessLevel: feature.merge_requests_access_level,
+ buildsAccessLevel: feature.builds_access_level,
+ wikiAccessLevel: feature.wiki_access_level,
+ snippetsAccessLevel: feature.snippets_access_level,
+ containerRegistryEnabled: !!project.container_registry_enabled,
+ lfsEnabled: !!project.lfs_enabled
+ }
+ end
+
+ def project_permissions_panel_data(project)
+ data = {
+ currentSettings: project_permissions_settings(project),
+ canChangeVisibilityLevel: can_change_visibility_level?(project, current_user),
+ allowedVisibilityOptions: project_allowed_visibility_levels(project),
+ visibilityHelpPath: help_page_path('public_access/public_access'),
+ registryAvailable: Gitlab.config.registry.enabled,
+ registryHelpPath: help_page_path('user/project/container_registry'),
+ lfsAvailable: Gitlab.config.lfs.enabled && current_user.admin?,
+ lfsHelpPath: help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
+ }
+
+ data.to_json.html_safe
+ end
+
+ def project_allowed_visibility_levels(project)
+ Gitlab::VisibilityLevel.values.select do |level|
+ project.visibility_level_allowed?(level) && !restricted_levels.include?(level)
+ end
+ end
+
def find_file_path
return unless @project && !@project.empty_repo?
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index af6683a548b..cf28a917fd1 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -92,7 +92,7 @@ module SearchHelper
# Autocomplete results for the current user's groups
def groups_autocomplete(term, limit = 5)
- current_user.authorized_groups.search(term).limit(limit).map do |group|
+ current_user.authorized_groups.order_id_desc.search(term).limit(limit).map do |group|
{
category: "Groups",
id: group.id,
@@ -104,7 +104,7 @@ module SearchHelper
# Autocomplete results for the current user's projects
def projects_autocomplete(term, limit = 5)
- current_user.authorized_projects.search_by_title(term)
+ current_user.authorized_projects.order_id_desc.search_by_title(term)
.sorted_by_stars.non_archived.limit(limit).map do |p|
{
category: "Projects",
diff --git a/app/models/broadcast_message.rb b/app/models/broadcast_message.rb
index fdc5a2adea0..0b561203914 100644
--- a/app/models/broadcast_message.rb
+++ b/app/models/broadcast_message.rb
@@ -33,7 +33,7 @@ class BroadcastMessage < ActiveRecord::Base
end
def self.current_and_future_messages
- where('ends_at > :now', now: Time.zone.now).reorder(id: :asc)
+ where('ends_at > :now', now: Time.zone.now).order_id_asc
end
def active?
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 64c93966dff..5ebe6f180e6 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -216,6 +216,7 @@ module Ci
variables += runner.predefined_variables if runner
variables += project.container_registry_variables
variables += project.deployment_variables if has_environment?
+ variables += project.auto_devops_variables
variables += yaml_variables
variables += user_variables
variables += project.group.secret_variables_for(ref, project).map(&:to_runner_variable) if project.group
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 46e5c344fdc..871c76fbad3 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -38,6 +38,7 @@ module Ci
validates :status, presence: { unless: :importing? }
validate :valid_commit_sha, unless: :importing?
+ after_initialize :set_config_source, if: :new_record?
after_create :keep_around_commits, unless: :importing?
enum source: {
@@ -50,6 +51,12 @@ module Ci
external: 6
}
+ enum config_source: {
+ unknown_source: nil,
+ repository_source: 1,
+ auto_devops_source: 2
+ }
+
state_machine :status, initial: :created do
event :enqueue do
transition created: :pending
@@ -316,6 +323,14 @@ module Ci
builds.latest.failed_but_allowed.any?
end
+ def set_config_source
+ if ci_yaml_from_repo
+ self.config_source = :repository_source
+ elsif implied_ci_yaml_file
+ self.config_source = :auto_devops_source
+ end
+ end
+
def config_processor
return unless ci_yaml_file
return @config_processor if defined?(@config_processor)
@@ -342,11 +357,17 @@ module Ci
def ci_yaml_file
return @ci_yaml_file if defined?(@ci_yaml_file)
- @ci_yaml_file = begin
- project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
- rescue Rugged::ReferenceError, GRPC::NotFound, GRPC::Internal
- self.yaml_errors =
- "Failed to load CI/CD config file at #{ci_yaml_file_path}"
+ @ci_yaml_file =
+ if auto_devops_source?
+ implied_ci_yaml_file
+ else
+ ci_yaml_from_repo
+ end
+
+ if @ci_yaml_file
+ @ci_yaml_file
+ else
+ self.yaml_errors = "Failed to load CI/CD config file for #{sha}"
nil
end
end
@@ -434,6 +455,23 @@ module Ci
private
+ def ci_yaml_from_repo
+ return unless project
+ return unless sha
+
+ project.repository.gitlab_ci_yml_for(sha, ci_yaml_file_path)
+ rescue GRPC::NotFound, Rugged::ReferenceError, GRPC::Internal
+ nil
+ end
+
+ def implied_ci_yaml_file
+ return unless project
+
+ if project.auto_devops_enabled?
+ Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content
+ end
+ end
+
def pipeline_data
Gitlab::DataBuilder::Pipeline.build(self)
end
diff --git a/app/models/concerns/sortable.rb b/app/models/concerns/sortable.rb
index a155a064032..db3cd257584 100644
--- a/app/models/concerns/sortable.rb
+++ b/app/models/concerns/sortable.rb
@@ -6,10 +6,6 @@ module Sortable
extend ActiveSupport::Concern
included do
- # By default all models should be ordered
- # by created_at field starting from newest
- default_scope { order_id_desc }
-
scope :order_id_desc, -> { reorder(id: :desc) }
scope :order_id_asc, -> { reorder(id: :asc) }
scope :order_created_desc, -> { reorder(created_at: :desc) }
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 435eeaf0e2e..9b05f8b1cd5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -82,12 +82,7 @@ class Environment < ActiveRecord::Base
def set_environment_type
names = name.split('/')
- self.environment_type =
- if names.many?
- names.first
- else
- nil
- end
+ self.environment_type = names.many? ? names.first : nil
end
def includes_commit?(commit)
@@ -101,7 +96,7 @@ class Environment < ActiveRecord::Base
end
def update_merge_request_metrics?
- (environment_type || name) == "production"
+ folder_name == "production"
end
def first_deployment_for(commit)
@@ -223,6 +218,10 @@ class Environment < ActiveRecord::Base
format: :json)
end
+ def folder_name
+ self.environment_type || self.name
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index e7cbc5170e8..4a9a23fea1f 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -44,6 +44,10 @@ class Namespace < ActiveRecord::Base
after_commit :refresh_access_of_projects_invited_groups, on: :update, if: -> { previous_changes.key?('share_with_group_lock') }
+ before_create :sync_share_with_group_lock_with_parent
+ before_update :sync_share_with_group_lock_with_parent, if: :parent_changed?
+ after_update :force_share_with_group_lock_on_descendants, if: -> { share_with_group_lock_changed? && share_with_group_lock? }
+
# Legacy Storage specific hooks
after_update :move_dir, if: :path_changed?
@@ -219,4 +223,14 @@ class Namespace < ActiveRecord::Base
errors.add(:parent_id, "has too deep level of nesting")
end
end
+
+ def sync_share_with_group_lock_with_parent
+ if parent&.share_with_group_lock?
+ self.share_with_group_lock = true
+ end
+ end
+
+ def force_share_with_group_lock_on_descendants
+ descendants.update_all(share_with_group_lock: true)
+ end
end
diff --git a/app/models/project.rb b/app/models/project.rb
index 18800921c6c..ff5638dd155 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -187,9 +187,12 @@ class Project < ActiveRecord::Base
has_many :active_runners, -> { active }, through: :runner_projects, source: :runner, class_name: 'Ci::Runner'
+ has_one :auto_devops, class_name: 'ProjectAutoDevops'
+
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature
accepts_nested_attributes_for :import_data
+ accepts_nested_attributes_for :auto_devops
delegate :name, to: :owner, allow_nil: true, prefix: true
delegate :members, to: :team, prefix: true
@@ -466,6 +469,18 @@ class Project < ActiveRecord::Base
self[:lfs_enabled] && Gitlab.config.lfs.enabled
end
+ def auto_devops_enabled?
+ if auto_devops&.enabled.nil?
+ current_application_settings.auto_devops_enabled?
+ else
+ auto_devops.enabled?
+ end
+ end
+
+ def has_auto_devops_implicitly_disabled?
+ auto_devops&.enabled.nil? && !current_application_settings.auto_devops_enabled?
+ end
+
def repository_storage_path
Gitlab.config.repositories.storages[repository_storage].try(:[], 'path')
end
@@ -1378,6 +1393,10 @@ class Project < ActiveRecord::Base
Gitlab::Utils.slugify(full_path.to_s)
end
+ def has_ci?
+ repository.gitlab_ci_yml || auto_devops_enabled?
+ end
+
def predefined_variables
[
{ key: 'CI_PROJECT_ID', value: id.to_s, public: true },
@@ -1423,6 +1442,12 @@ class Project < ActiveRecord::Base
deployment_service.predefined_variables
end
+ def auto_devops_variables
+ return [] unless auto_devops_enabled?
+
+ auto_devops&.variables || []
+ end
+
def append_or_update_attribute(name, value)
old_values = public_send(name.to_s) # rubocop:disable GitlabSecurity/PublicSend
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
new file mode 100644
index 00000000000..53731579e87
--- /dev/null
+++ b/app/models/project_auto_devops.rb
@@ -0,0 +1,11 @@
+class ProjectAutoDevops < ActiveRecord::Base
+ belongs_to :project
+
+ validates :domain, allow_blank: true, hostname: { allow_numeric_hostname: true }
+
+ def variables
+ variables = []
+ variables << { key: 'AUTO_DEVOPS_DOMAIN', value: domain, public: true } if domain.present?
+ variables
+ end
+end
diff --git a/app/models/user.rb b/app/models/user.rb
index 105eb62f1fa..d7549409b15 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -35,6 +35,7 @@ class User < ActiveRecord::Base
default_value_for :project_view, :files
default_value_for :notified_of_own_activity, false
default_value_for :preferred_language, I18n.default_locale
+ default_value_for :theme_id, gitlab_config.default_theme
attr_encrypted :otp_secret,
key: Gitlab::Application.secrets.otp_key_base,
@@ -72,7 +73,7 @@ class User < ActiveRecord::Base
#
# Namespace for personal projects
- has_one :namespace, -> { where type: nil }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> do
@@ -259,11 +260,13 @@ class User < ActiveRecord::Base
end
def sort(method)
- case method.to_s
+ order_method = method || 'id_desc'
+
+ case order_method.to_s
when 'recent_sign_in' then order_recent_sign_in
when 'oldest_sign_in' then order_oldest_sign_in
else
- order_by(method)
+ order_by(order_method)
end
end
@@ -371,7 +374,7 @@ class User < ActiveRecord::Base
# Returns a user for the given SSH key.
def find_by_ssh_key_id(key_id)
- find_by(id: Key.unscoped.select(:user_id).where(id: key_id))
+ Key.find_by(id: key_id)&.user
end
def find_by_full_path(path, follow_redirects: false)
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 8ada661e571..420991ff6d6 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -15,6 +15,11 @@ class GroupPolicy < BasePolicy
condition(:nested_groups_supported, scope: :global) { Group.supports_nested_groups? }
+ condition(:has_parent, scope: :subject) { @subject.has_parent? }
+ condition(:share_with_group_locked, scope: :subject) { @subject.share_with_group_lock? }
+ condition(:parent_share_with_group_locked, scope: :subject) { @subject.parent&.share_with_group_lock? }
+ condition(:can_change_parent_share_with_group_lock) { can?(:change_share_with_group_lock, @subject.parent) }
+
condition(:has_projects) do
GroupProjectsFinder.new(group: @subject, current_user: @user).execute.any?
end
@@ -44,7 +49,7 @@ class GroupPolicy < BasePolicy
enable :change_visibility_level
end
- rule { owner & can_create_group & nested_groups_supported }.enable :create_subgroup
+ rule { owner & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
@@ -54,6 +59,8 @@ class GroupPolicy < BasePolicy
rule { ~can?(:view_globally) }.prevent :request_access
rule { has_access }.prevent :request_access
+ rule { owner & (~share_with_group_locked | ~has_parent | ~parent_share_with_group_locked | can_change_parent_share_with_group_lock) }.enable :change_share_with_group_lock
+
def access_level
return GroupMember::NO_ACCESS if @user.nil?
diff --git a/app/serializers/environment_entity.rb b/app/serializers/environment_entity.rb
index dcaccc3007d..ba0ae6ba8a0 100644
--- a/app/serializers/environment_entity.rb
+++ b/app/serializers/environment_entity.rb
@@ -26,5 +26,9 @@ class EnvironmentEntity < Grape::Entity
terminal_project_environment_path(environment.project, environment)
end
+ expose :folder_path do |environment|
+ folder_project_environments_path(environment.project, environment.folder_name)
+ end
+
expose :created_at, :updated_at
end
diff --git a/app/serializers/environment_serializer.rb b/app/serializers/environment_serializer.rb
index d0a60f134da..88842a9aa75 100644
--- a/app/serializers/environment_serializer.rb
+++ b/app/serializers/environment_serializer.rb
@@ -36,9 +36,9 @@ class EnvironmentSerializer < BaseSerializer
private
def itemize(resource)
- items = resource.order('folder_name ASC')
+ items = resource.order('folder ASC')
.group('COALESCE(environment_type, name)')
- .select('COALESCE(environment_type, name) AS folder_name',
+ .select('COALESCE(environment_type, name) AS folder',
'COUNT(*) AS size', 'MAX(id) AS last_id')
# It makes a difference when you call `paginate` method, because
@@ -49,7 +49,7 @@ class EnvironmentSerializer < BaseSerializer
environments = resource.where(id: items.map(&:last_id)).index_by(&:id)
items.map do |item|
- Item.new(item.folder_name, item.size, environments[item.last_id])
+ Item.new(item.folder, item.size, environments[item.last_id])
end
end
end
diff --git a/app/serializers/pipeline_entity.rb b/app/serializers/pipeline_entity.rb
index c4f000b0ca3..357fc71f877 100644
--- a/app/serializers/pipeline_entity.rb
+++ b/app/serializers/pipeline_entity.rb
@@ -16,6 +16,7 @@ class PipelineEntity < Grape::Entity
expose :flags do
expose :latest?, as: :latest
expose :stuck?, as: :stuck
+ expose :auto_devops_source?, as: :auto_devops
expose :has_yaml_errors?, as: :yaml_errors
expose :can_retry?, as: :retryable
expose :can_cancel?, as: :cancelable
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 414c01b2546..d20de9b16a4 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -16,9 +16,9 @@ module Ci
protected: project.protected_for?(ref)
)
- result = validate(current_user,
- ignore_skip_ci: ignore_skip_ci,
- save_on_errors: save_on_errors)
+ result = validate_project_and_git_items ||
+ validate_pipeline(ignore_skip_ci: ignore_skip_ci,
+ save_on_errors: save_on_errors)
return result if result
@@ -47,13 +47,13 @@ module Ci
private
- def validate(triggering_user, ignore_skip_ci:, save_on_errors:)
+ def validate_project_and_git_items
unless project.builds_enabled?
return error('Pipeline is disabled')
end
- unless allowed_to_trigger_pipeline?(triggering_user)
- if can?(triggering_user, :create_pipeline, project)
+ unless allowed_to_trigger_pipeline?
+ if can?(current_user, :create_pipeline, project)
return error("Insufficient permissions for protected ref '#{ref}'")
else
return error('Insufficient permissions to create a new pipeline')
@@ -67,7 +67,9 @@ module Ci
unless commit
return error('Commit not found')
end
+ end
+ def validate_pipeline(ignore_skip_ci:, save_on_errors:)
unless pipeline.config_processor
unless pipeline.ci_yaml_file
return error("Missing #{pipeline.ci_yaml_file_path} file")
@@ -85,25 +87,25 @@ module Ci
end
end
- def allowed_to_trigger_pipeline?(triggering_user)
- if triggering_user
- allowed_to_create?(triggering_user)
+ def allowed_to_trigger_pipeline?
+ if current_user
+ allowed_to_create?
else # legacy triggers don't have a corresponding user
!project.protected_for?(ref)
end
end
- def allowed_to_create?(triggering_user)
- access = Gitlab::UserAccess.new(triggering_user, project: project)
+ def allowed_to_create?
+ return unless can?(current_user, :create_pipeline, project)
- 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
+ access = Gitlab::UserAccess.new(current_user, project: 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
def update_merge_requests_head_pipeline
diff --git a/app/services/concerns/update_visibility_level.rb b/app/services/concerns/update_visibility_level.rb
new file mode 100644
index 00000000000..536fcc6acce
--- /dev/null
+++ b/app/services/concerns/update_visibility_level.rb
@@ -0,0 +1,15 @@
+module UpdateVisibilityLevel
+ def valid_visibility_level_change?(target, new_visibility)
+ # check that user is allowed to set specified visibility_level
+ if new_visibility && new_visibility.to_i != target.visibility_level
+ unless can?(current_user, :change_visibility_level, target) &&
+ Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+
+ deny_visibility_level(target, new_visibility)
+ return false
+ end
+ end
+
+ true
+ end
+end
diff --git a/app/services/groups/create_service.rb b/app/services/groups/create_service.rb
index c7c27621085..70e50aa0f12 100644
--- a/app/services/groups/create_service.rb
+++ b/app/services/groups/create_service.rb
@@ -8,15 +8,7 @@ module Groups
def execute
@group = Group.new(params)
- unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
- deny_visibility_level(@group)
- return @group
- end
-
- if @group.parent && !can?(current_user, :create_subgroup, @group.parent)
- @group.parent = nil
- @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.')
-
+ unless can_use_visibility_level? && can_create_group?
return @group
end
@@ -39,5 +31,33 @@ module Groups
def create_chat_team?
Gitlab.config.mattermost.enabled && @chat_team && group.chat_team.nil?
end
+
+ def can_create_group?
+ if @group.subgroup?
+ unless can?(current_user, :create_subgroup, @group.parent)
+ @group.parent = nil
+ @group.errors.add(:parent_id, 'You don’t have permission to create a subgroup in this group.')
+
+ return false
+ end
+ else
+ unless can?(current_user, :create_group)
+ @group.errors.add(:base, 'You don’t have permission to create groups.')
+
+ return false
+ end
+ end
+
+ true
+ end
+
+ def can_use_visibility_level?
+ unless Gitlab::VisibilityLevel.allowed_for?(current_user, params[:visibility_level])
+ deny_visibility_level(@group)
+ return false
+ end
+
+ true
+ end
end
end
diff --git a/app/services/groups/update_service.rb b/app/services/groups/update_service.rb
index 1d65c76d282..08e3efb96e3 100644
--- a/app/services/groups/update_service.rb
+++ b/app/services/groups/update_service.rb
@@ -1,18 +1,13 @@
module Groups
class UpdateService < Groups::BaseService
+ include UpdateVisibilityLevel
+
def execute
reject_parent_id!
- # check that user is allowed to set specified visibility_level
- new_visibility = params[:visibility_level]
- if new_visibility && new_visibility.to_i != group.visibility_level
- unless can?(current_user, :change_visibility_level, group) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
+ return false unless valid_visibility_level_change?(group, params[:visibility_level])
- deny_visibility_level(group, new_visibility)
- return group
- end
- end
+ return false unless valid_share_with_group_lock_change?
group.assign_attributes(params)
@@ -30,5 +25,19 @@ module Groups
def reject_parent_id!
params.except!(:parent_id)
end
+
+ def valid_share_with_group_lock_change?
+ return true unless changing_share_with_group_lock?
+ return true if can?(current_user, :change_share_with_group_lock, group)
+
+ group.errors.add(:share_with_group_lock, s_('GroupSettings|cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group'))
+ false
+ end
+
+ def changing_share_with_group_lock?
+ return false if params[:share_with_group_lock].nil?
+
+ params[:share_with_group_lock] != group.share_with_group_lock
+ end
end
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index cf69007bc3b..cb4ffcab778 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -1,7 +1,9 @@
module Projects
class UpdateService < BaseService
+ include UpdateVisibilityLevel
+
def execute
- unless visibility_level_allowed?
+ unless valid_visibility_level_change?(project, params[:visibility_level])
return error('New visibility level not allowed!')
end
@@ -28,22 +30,6 @@ module Projects
private
- def visibility_level_allowed?
- # check that user is allowed to set specified visibility_level
- new_visibility = params[:visibility_level]
-
- if new_visibility && new_visibility.to_i != project.visibility_level
- unless can?(current_user, :change_visibility_level, project) &&
- Gitlab::VisibilityLevel.allowed_for?(current_user, new_visibility)
-
- deny_visibility_level(project, new_visibility)
- return false
- end
- end
-
- true
- end
-
def renaming_project_with_container_registry_tags?
new_path = params[:path]
diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb
index 9cdb9935bea..a077b3584b0 100644
--- a/app/services/quick_actions/interpret_service.rb
+++ b/app/services/quick_actions/interpret_service.rb
@@ -115,7 +115,7 @@ module QuickActions
if issuable.allows_multiple_assignees?
issuable.assignees.pluck(:id) + users.map(&:id)
else
- [users.last.id]
+ [users.first.id]
end
end
diff --git a/app/services/test_hooks/base_service.rb b/app/services/test_hooks/base_service.rb
index 4abd2c44b2f..20d90504bd2 100644
--- a/app/services/test_hooks/base_service.rb
+++ b/app/services/test_hooks/base_service.rb
@@ -9,18 +9,17 @@ module TestHooks
end
def execute
+ trigger_key = hook.class::TRIGGERS.key(trigger.to_sym)
trigger_data_method = "#{trigger}_data"
- if !self.respond_to?(trigger_data_method, true) ||
- !hook.class::TRIGGERS.value?(trigger.to_sym)
-
+ if trigger_key.nil? || !self.respond_to?(trigger_data_method, true)
return error('Testing not available for this hook')
end
error_message = catch(:validation_error) do
sample_data = self.__send__(trigger_data_method) # rubocop:disable GitlabSecurity/PublicSend
- return hook.execute(sample_data, trigger)
+ return hook.execute(sample_data, trigger_key)
end
error(error_message)
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 2825478926a..cd99e0b90f9 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -19,7 +19,7 @@ class WebHookService
def initialize(hook, data, hook_name)
@hook = hook
@data = data
- @hook_name = hook_name
+ @hook_name = hook_name.to_s
end
def execute
diff --git a/app/views/admin/application_settings/_form.html.haml b/app/views/admin/application_settings/_form.html.haml
index a010b4691bf..dbaed1d09fb 100644
--- a/app/views/admin/application_settings/_form.html.haml
+++ b/app/views/admin/application_settings/_form.html.haml
@@ -226,7 +226,17 @@
.help-block 0 for unlimited
%fieldset
- %legend Continuous Integration
+ %legend Continuous Integration and Deployment
+ .form-group
+ .col-sm-offset-2.col-sm-10
+ .checkbox
+ = f.label :auto_devops_enabled do
+ = f.check_box :auto_devops_enabled
+ Enabled Auto DevOps (Beta) for projects by default
+ .help-block
+ It will automatically build, test, and deploy applications based on a predefined CI/CD configuration
+ = link_to icon('question-circle'), help_page_path('topics/autodevops/index.md')
+
.form-group
.col-sm-offset-2.col-sm-10
.checkbox
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 839f23e69fd..0d3308833b7 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -28,17 +28,20 @@
.col-sm-offset-2.col-sm-10
= render 'shared/allow_request_access', form: f
- = render 'group_admin_settings', f: f
-
.form-group
- %hr
- = f.label :share_with_group_lock, class: 'control-label' do
- Share with group lock
+ %label.control-label
+ = s_("GroupSettings|Share with group lock")
.col-sm-10
.checkbox
- = f.check_box :share_with_group_lock
- %span.descr Prevent sharing a project with another group within this group
+ = f.label :share_with_group_lock do
+ = f.check_box :share_with_group_lock, disabled: !can_change_share_with_group_lock?(@group)
+ %strong
+ - group_link = link_to @group.name, group_path(@group)
+ = s_("GroupSettings|Prevent sharing a project within %{group} with other groups").html_safe % { group: group_link }
+ %br
+ %span.descr= share_with_group_lock_help_text(@group)
+ = render 'group_admin_settings', f: f
.form-actions
= f.submit 'Save group', class: "btn btn-save"
diff --git a/app/views/layouts/application.html.haml b/app/views/layouts/application.html.haml
index 65ac8aaa59b..0ca34b276a7 100644
--- a/app/views/layouts/application.html.haml
+++ b/app/views/layouts/application.html.haml
@@ -1,7 +1,7 @@
!!! 5
%html{ lang: I18n.locale, class: page_class }
= render "layouts/head"
- %body{ class: @body_class, data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
+ %body{ class: "#{user_application_theme} #{@body_class}", data: { page: body_data_page, project: "#{@project.path if @project}", group: "#{@group.path if @group}", find_file: find_file_path } }
= render "layouts/init_auto_complete" if @gfm_form
= render 'peek/bar'
= render "layouts/header/default"
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 8a39c4d775f..c254ee02dd8 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -1,5 +1,5 @@
%ul.list-unstyled.navbar-sub-nav
- = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown" }) do
+ = nav_link(path: ['root#index', 'projects#trending', 'projects#starred', 'dashboard/projects#index'], html_options: { id: 'nav-projects-dropdown', class: "home dropdown header-projects" }) do
%a{ href: "#", data: { toggle: "dropdown" } }
Projects
= custom_icon('caret_down')
@@ -22,7 +22,7 @@
= link_to dashboard_snippets_path, class: 'dashboard-shortcuts-snippets', title: 'Snippets' do
Snippets
- %li.dropdown.hidden-lg
+ %li.header-more.dropdown.hidden-lg
%a{ href: "#", data: { toggle: "dropdown" } }
More
= custom_icon('caret_down')
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index 9e7fe556d88..2b72eeab8d6 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -3,6 +3,26 @@
= render 'profiles/head'
= form_for @user, url: profile_preferences_path, remote: true, method: :put, html: { class: 'row prepend-top-default js-preferences-form' } do |f|
+ .col-lg-4.application-theme
+ %h4.prepend-top-0
+ GitLab navigation theme
+ %p Customize the appearance of the application header and navigation sidebar.
+ .col-lg-8.application-theme
+ - Gitlab::Themes.each do |theme|
+ = label_tag do
+ .preview{ class: theme.name.downcase }
+ .preview-row
+ .quadrant.one
+ .quadrant.two
+ .preview-row
+ .quadrant.three
+ .quadrant.four
+ = f.radio_button :theme_id, theme.id
+ = theme.name
+
+ .col-sm-12
+ %hr
+
.col-lg-4.profile-settings-sidebar
%h4.prepend-top-0
Syntax highlighting theme
diff --git a/app/views/profiles/preferences/update.js.erb b/app/views/profiles/preferences/update.js.erb
index 431ab9d052b..8966dd3fd86 100644
--- a/app/views/profiles/preferences/update.js.erb
+++ b/app/views/profiles/preferences/update.js.erb
@@ -1,3 +1,7 @@
+// Remove body class for any previous theme, re-add current one
+$('body').removeClass('<%= Gitlab::Themes.body_classes %>')
+$('body').addClass('<%= user_application_theme %>')
+
// Toggle container-fluid class
if ('<%= current_user.layout %>' === 'fluid') {
$('.content-wrapper .container-fluid').removeClass('container-limited')
diff --git a/app/views/projects/_merge_request_merge_settings.html.haml b/app/views/projects/_merge_request_merge_settings.html.haml
index e3effe45d6c..1dd8778f800 100644
--- a/app/views/projects/_merge_request_merge_settings.html.haml
+++ b/app/views/projects/_merge_request_merge_settings.html.haml
@@ -1,7 +1,7 @@
- form = local_assigns.fetch(:form)
.form-group
- .checkbox.builds-feature
+ .checkbox.builds-feature{ class: ("hidden" if @project && @project.project_feature.send(:builds_access_level) == 0) }
= form.label :only_allow_merge_if_pipeline_succeeds do
= form.check_box :only_allow_merge_if_pipeline_succeeds
%strong Only allow merge requests to be merged if the pipeline succeeds
diff --git a/app/views/projects/blob/viewers/_route_map.html.haml b/app/views/projects/blob/viewers/_route_map.html.haml
index d0fcd55f6c1..6d6bd79bc3c 100644
--- a/app/views/projects/blob/viewers/_route_map.html.haml
+++ b/app/views/projects/blob/viewers/_route_map.html.haml
@@ -6,4 +6,4 @@
This Route Map is invalid:
= viewer.validation_message
-= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
diff --git a/app/views/projects/blob/viewers/_route_map_loading.html.haml b/app/views/projects/blob/viewers/_route_map_loading.html.haml
index 2318cf82f58..a5f73fb0197 100644
--- a/app/views/projects/blob/viewers/_route_map_loading.html.haml
+++ b/app/views/projects/blob/viewers/_route_map_loading.html.haml
@@ -1,4 +1,4 @@
= icon('spinner spin fw')
Validating Route Map…
-= link_to 'Learn more', help_page_path('ci/environments', anchor: 'route-map')
+= link_to 'Learn more', help_page_path('ci/environments', anchor: 'go-directly-from-source-files-to-public-pages-on-the-environment')
diff --git a/app/views/projects/commit/_pipelines_list.haml b/app/views/projects/commit/_pipelines_list.haml
index 09e3a775d1c..bef96786b73 100644
--- a/app/views/projects/commit/_pipelines_list.haml
+++ b/app/views/projects/commit/_pipelines_list.haml
@@ -2,6 +2,7 @@
#commit-pipeline-table-view{ data: { disable_initialization: disable_initialization,
endpoint: endpoint,
"help-page-path" => help_page_path('ci/quick_start/README'),
+ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
} }
- content_for :page_specific_javascripts do
diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml
index 994119051d2..0a3045604f4 100644
--- a/app/views/projects/edit.html.haml
+++ b/app/views/projects/edit.html.haml
@@ -66,90 +66,18 @@
%section.settings.sharing-permissions
.settings-header
%h4
- Sharing and permissions
+ Permissions
%button.btn.js-settings-toggle
= expanded ? 'Collapse' : 'Expand'
%p
Enable or disable certain project features and choose access levels.
.settings-content.no-animate{ class: ('expanded' if expanded) }
= form_for [@project.namespace.becomes(Namespace), @project], remote: true, html: { multipart: true, class: "sharing-permissions-form" }, authenticity_token: true do |f|
- .form_group.sharing-and-permissions
- .row.js-visibility-select
- .col-md-8
- .label-light
- = label_tag :project_visibility, 'Project Visibility', class: 'label-light', for: :project_visibility_level
- = link_to icon('question-circle'), help_page_path("public_access/public_access")
- %span.help-block
- .col-md-4.visibility-select-container
- = render('projects/visibility_select', model_method: :visibility_level, form: f, selected_level: @project.visibility_level)
- = f.fields_for :project_feature do |feature_fields|
- %fieldset.features
- .row
- .col-md-8.project-feature
- = feature_fields.label :repository_access_level, "Repository", class: 'label-light'
- %span.help-block View and edit files in this project
- .col-md-4.js-repo-access-level
- = project_feature_access_select(:repository_access_level)
-
- .row
- .col-md-8.project-feature.nested
- = feature_fields.label :merge_requests_access_level, "Merge requests", class: 'label-light'
- %span.help-block Submit changes to be merged upstream
- .col-md-4
- = project_feature_access_select(:merge_requests_access_level)
-
- .row
- .col-md-8.project-feature.nested
- = feature_fields.label :builds_access_level, "Pipelines", class: 'label-light'
- %span.help-block Build, test, and deploy your changes
- .col-md-4
- = project_feature_access_select(:builds_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :snippets_access_level, "Snippets", class: 'label-light'
- %span.help-block Share code pastes with others out of Git repository
- .col-md-4
- = project_feature_access_select(:snippets_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :issues_access_level, "Issues", class: 'label-light'
- %span.help-block Lightweight issue tracking system for this project
- .col-md-4
- = project_feature_access_select(:issues_access_level)
-
- .row
- .col-md-8.project-feature
- = feature_fields.label :wiki_access_level, "Wiki", class: 'label-light'
- %span.help-block Pages for project documentation
- .col-md-4
- = project_feature_access_select(:wiki_access_level)
- .form-group
- = render 'shared/allow_request_access', form: f
- - if Gitlab.config.lfs.enabled && current_user.admin?
- .row.js-lfs-enabled.form-group.sharing-and-permissions
- .col-md-8
- = f.label :lfs_enabled, 'Git Large File Storage', class: 'label-light'
- = link_to icon('question-circle'), help_page_path('workflow/lfs/manage_large_binaries_with_git_lfs')
- %span.help-block Manages large files such as audio, video and graphics files.
- .col-md-4
- .select-wrapper
- = f.select :lfs_enabled, [%w(Enabled true), %w(Disabled false)], {}, selected: @project.lfs_enabled?, class: 'pull-right form-control project-repo-select select-control', data: { field: 'lfs_enabled' }
- = icon('chevron-down')
- - if Gitlab.config.registry.enabled
- .form-group.js-container-registry{ style: ("display: none;" if @project.project_feature.send(:repository_access_level) == 0) }
- .checkbox
- = f.label :container_registry_enabled do
- = f.check_box :container_registry_enabled
- %strong Container Registry
- %br
- %span.descr Enable Container Registry for this project
- = link_to icon('question-circle'), help_page_path('user/project/container_registry'), target: '_blank'
+ %script.js-project-permissions-form-data{ type: "application/json" }= project_permissions_panel_data(@project)
+ .js-project-permissions-form
= f.submit 'Save changes', class: "btn btn-save"
-
- %section.settings.merge-requests-feature{ style: ("display: none;" if @project.project_feature.send(:merge_requests_access_level) == 0) }
+ %section.settings.merge-requests-feature{ class: ("hidden" if @project.project_feature.send(:merge_requests_access_level) == 0) }
.settings-header
%h4
Merge request settings
diff --git a/app/views/projects/merge_requests/index.html.haml b/app/views/projects/merge_requests/index.html.haml
index fc7190505da..2c53891a92d 100644
--- a/app/views/projects/merge_requests/index.html.haml
+++ b/app/views/projects/merge_requests/index.html.haml
@@ -16,6 +16,8 @@
- if @project.merge_requests.exists?
%div{ class: container_class }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
.top-area
= render 'shared/issuable/nav', type: :merge_requests
.nav-controls
diff --git a/app/views/projects/pipelines/index.html.haml b/app/views/projects/pipelines/index.html.haml
index c1729850cf4..9fc15d20f34 100644
--- a/app/views/projects/pipelines/index.html.haml
+++ b/app/views/projects/pipelines/index.html.haml
@@ -2,19 +2,23 @@
- page_title "Pipelines"
= render "projects/pipelines/head"
-#pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
- "css-class" => container_class,
- "help-page-path" => help_page_path('ci/quick_start/README'),
- "new-pipeline-path" => new_project_pipeline_path(@project),
- "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
- "all-path" => project_pipelines_path(@project),
- "pending-path" => project_pipelines_path(@project, scope: :pending),
- "running-path" => project_pipelines_path(@project, scope: :running),
- "finished-path" => project_pipelines_path(@project, scope: :finished),
- "branches-path" => project_pipelines_path(@project, scope: :branches),
- "tags-path" => project_pipelines_path(@project, scope: :tags),
- "has-ci" => @repository.gitlab_ci_yml,
- "ci-lint-path" => ci_lint_path } }
+%div{ 'class' => container_class }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
-= page_specific_javascript_bundle_tag('common_vue')
-= page_specific_javascript_bundle_tag('pipelines')
+ #pipelines-list-vue{ data: { endpoint: project_pipelines_path(@project, format: :json),
+ "help-page-path" => help_page_path('ci/quick_start/README'),
+ "help-auto-devops-path" => help_page_path('topics/autodevops/index.md'),
+ "new-pipeline-path" => new_project_pipeline_path(@project),
+ "can-create-pipeline" => can?(current_user, :create_pipeline, @project).to_s,
+ "all-path" => project_pipelines_path(@project),
+ "pending-path" => project_pipelines_path(@project, scope: :pending),
+ "running-path" => project_pipelines_path(@project, scope: :running),
+ "finished-path" => project_pipelines_path(@project, scope: :finished),
+ "branches-path" => project_pipelines_path(@project, scope: :branches),
+ "tags-path" => project_pipelines_path(@project, scope: :tags),
+ "has-ci" => @project.has_ci?,
+ "ci-lint-path" => ci_lint_path } }
+
+ = page_specific_javascript_bundle_tag('common_vue')
+ = page_specific_javascript_bundle_tag('pipelines')
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 8bf76b646f7..324cd423ede 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -2,11 +2,42 @@
.col-lg-12
= form_for @project, url: project_pipelines_settings_path(@project) do |f|
%fieldset.builds-feature
- - unless @repository.gitlab_ci_yml
- .form-group
- %p Pipelines need to be configured before you can begin using Continuous Integration.
- = link_to 'Get started with Pipelines', help_page_path('ci/quick_start/README'), class: 'btn btn-info'
- %hr
+ .form-group
+ %p Pipelines need to have Auto DevOps enabled or have a .gitlab-ci.yml configured before you can begin using Continuous Integration and Delivery.
+ %h5 Auto DevOps (Beta)
+ %p
+ Auto DevOps will automatically build, test, and deploy your application based on a predefined Continious Integration and Delivery configuration.
+ = link_to 'Learn more about Auto DevOps', help_page_path('topics/autodevops/index.md')
+ = f.fields_for :auto_devops_attributes, @auto_devops do |form|
+ .radio
+ = form.label :enabled_true do
+ = form.radio_button :enabled, 'true'
+ %strong Enable Auto DevOps
+ %br
+ %span.descr
+ The Auto DevOps pipeline configuration will be used when there is no .gitlab-ci.yml
+ in the project.
+ .radio
+ = form.label :enabled_false do
+ = form.radio_button :enabled, 'false'
+ %strong Disable Auto DevOps
+ %br
+ %span.descr
+ A specific .gitlab-ci.yml file needs to be specified before you can begin using Continious Integration and Delivery.
+ .radio
+ = form.label :enabled do
+ = form.radio_button :enabled, nil
+ %strong
+ Instance default (status: #{current_application_settings.auto_devops_enabled?})
+ %br
+ %span.descr
+ Follow the instance default to either have Auto DevOps enabled or disabled when there is no project specific .gitlab-ci.yml file specified.
+ %br
+ %p
+ Define a domain used by Auto DevOps to deploy towards, this is required for deploys to succeed.
+ = form.text_field :domain, class: 'form-control', placeholder: 'domain.com'
+
+ %hr
.form-group.append-bottom-default
= f.label :runners_token, "Runner token", class: 'label-light'
= f.text_field :runners_token, class: "form-control", placeholder: 'xEeFCaDAB89'
diff --git a/app/views/projects/project_members/import.html.haml b/app/views/projects/project_members/import.html.haml
index f6ca8d5a921..755128af565 100644
--- a/app/views/projects/project_members/import.html.haml
+++ b/app/views/projects/project_members/import.html.haml
@@ -8,7 +8,7 @@
= form_tag apply_import_project_project_members_path(@project), method: 'post', class: 'form-horizontal' do
.form-group
= label_tag :source_project_id, "Project", class: 'control-label'
- .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(current_user.authorized_projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
+ .col-sm-10= select_tag(:source_project_id, options_from_collection_for_select(@projects, :id, :name_with_namespace), prompt: "Select project", class: "select2 lg", required: true)
.form-actions
= button_tag 'Import project members', class: "btn btn-create"
diff --git a/app/views/projects/settings/ci_cd/show.html.haml b/app/views/projects/settings/ci_cd/show.html.haml
index eaf374bcb83..d4f71d023c6 100644
--- a/app/views/projects/settings/ci_cd/show.html.haml
+++ b/app/views/projects/settings/ci_cd/show.html.haml
@@ -6,7 +6,7 @@
- expanded = Rails.env.test?
-%section.settings
+%section.settings#js-general-pipeline-settings
.settings-header
%h4
General pipelines settings
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 6ee55bba82a..d8f5114f4b5 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -82,5 +82,8 @@
- view_path = default_project_view
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
+
%div{ class: project_child_container_class(view_path) }
= render view_path
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 375e6764add..d84a1fd7ee1 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -14,5 +14,7 @@
= render "projects/commits/head"
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
+ - if show_auto_devops_callout?(@project)
+ = render 'shared/auto_devops_callout'
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
new file mode 100644
index 00000000000..2f09c2fec87
--- /dev/null
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -0,0 +1,15 @@
+.user-callout{ data: { uid: 'auto_devops_settings_dismissed', project_path: project_path(@project) } }
+ .bordered-box.landing.content-block
+ %button.btn.btn-default.close.js-close-callout{ type: 'button',
+ 'aria-label' => 'Dismiss Auto DevOps box' }
+ = icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
+ .svg-container
+ = custom_icon('icon_autodevops')
+ .user-callout-copy
+ %h4= _('Auto DevOps (Beta)')
+ %p= _('Auto DevOps can be activated for this project. It will automatically build, test, and deploy your application based on a predefined CI/CD configuration.')
+ %p
+ #{s_('AutoDevOps|Learn more in the')}
+ = link_to _('Auto DevOps documentation'), help_page_path('topics/autodevops/index.md'), target: '_blank', rel: 'noopener noreferrer'
+
+ = link_to _('Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'js-general-pipeline-settings'), class: 'btn btn-primary js-close-callout'
diff --git a/app/views/shared/_new_project_item_select.html.haml b/app/views/shared/_new_project_item_select.html.haml
index dc912d800cf..ac2ebb701a5 100644
--- a/app/views/shared/_new_project_item_select.html.haml
+++ b/app/views/shared/_new_project_item_select.html.haml
@@ -1,5 +1,5 @@
- if any_projects?(@projects)
- .project-item-select-holder.btn-group.pull-right
+ .project-item-select-holder.btn-group
%a.btn.btn-new.new-project-item-link{ href: '', data: { label: local_assigns[:label], type: local_assigns[:type] } }
= icon('spinner spin')
= 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]
diff --git a/app/views/shared/icons/_icon_autodevops.svg b/app/views/shared/icons/_icon_autodevops.svg
new file mode 100644
index 00000000000..807ff27bb67
--- /dev/null
+++ b/app/views/shared/icons/_icon_autodevops.svg
@@ -0,0 +1,54 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="189" height="179" viewBox="0 0 189 179">
+ <g fill="none" fill-rule="evenodd">
+ <path fill="#FFFFFF" fill-rule="nonzero" d="M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M110.160166,51.6956996 C106.846457,51.6956996 104.160166,54.3819911 104.160166,57.6956996 L104.160166,117.6957 C104.160166,121.009408 106.846457,123.6957 110.160166,123.6957 L160.160166,123.6957 C163.473874,123.6957 166.160166,121.009408 166.160166,117.6957 L166.160166,57.6956996 C166.160166,54.3819911 163.473874,51.6956996 160.160166,51.6956996 L110.160166,51.6956996 Z M110.160166,47.6956996 L160.160166,47.6956996 C165.683013,47.6956996 170.160166,52.1728521 170.160166,57.6956996 L170.160166,117.6957 C170.160166,123.218547 165.683013,127.6957 160.160166,127.6957 L110.160166,127.6957 C104.637318,127.6957 100.160166,123.218547 100.160166,117.6957 L100.160166,57.6956996 C100.160166,52.1728521 104.637318,47.6956996 110.160166,47.6956996 Z" transform="rotate(10 135.16 87.696)"/>
+ <rect width="70" height="80" x="80" y="34" fill="#000000" fill-opacity=".03" transform="rotate(5 115 74)" rx="10"/>
+ <path fill="#000000" fill-opacity=".03" fill-rule="nonzero" d="M126.028,178 L150,178 C169.972,177.457 186,161.1 186,141 C186,120.565 169.435,104 149,104 C148.007,104 147.023,104.04 146.05,104.116 C141.1,89.51 127.277,79 111,79 C105.71511,78.9924557 100.490375,80.1212986 95.68,82.31 C89.03,72.47 77.77,66 65,66 C44.565,66 28,82.565 28,103 C28,103.7 28.02,104.397 28.058,105.088 C11.944,109.088 0,123.648 0,141 C0,161.435 16.565,178 37,178 C37.324,178 37.646,177.996 37.968,177.988 L126.028,178 Z"/>
+ <g transform="rotate(5 -300.07 1010.998)">
+ <rect width="70" height="80" fill="#FFFFFF" rx="10"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M10,4 C6.6862915,4 4,6.6862915 4,10 L4,70 C4,73.3137085 6.6862915,76 10,76 L60,76 C63.3137085,76 66,73.3137085 66,70 L66,10 C66,6.6862915 63.3137085,4 60,4 L10,4 Z M10,-2.68673972e-14 L60,-2.68673972e-14 C65.5228475,-2.78819278e-14 70,4.4771525 70,10 L70,70 C70,75.5228475 65.5228475,80 60,80 L10,80 C4.4771525,80 9.15550472e-14,75.5228475 9.08786935e-14,70 L9.08786935e-14,10 C9.02023397e-14,4.4771525 4.4771525,-2.58528666e-14 10,-2.68673972e-14 Z"/>
+ <rect width="8" height="4" x="14" y="18" fill="#FC6D26" rx="2"/>
+ <rect width="8" height="4" x="32" y="38" fill="#E1DBF1" rx="2"/>
+ <rect width="8" height="4" x="45" y="48" fill="#FEE1D3" rx="2"/>
+ <rect width="8" height="4" x="44" y="38" fill="#FEF0E8" rx="2"/>
+ <rect width="12" height="4" x="26" y="18" fill="#EFEDF8" rx="2"/>
+ <rect width="6" height="4" x="15" y="48" fill="#FEF0E8" rx="2"/>
+ <rect width="6" height="4" x="25" y="48" fill="#FC6D26" rx="2"/>
+ <rect width="6" height="4" x="35" y="48" fill="#EEEEEE" rx="2"/>
+ <rect width="12" height="4" x="14" y="28" fill="#EEEEEE" rx="2"/>
+ <rect width="12" height="4" x="44" y="58" fill="#6B4FBB" rx="2"/>
+ <rect width="26" height="4" x="30" y="28" fill="#C3B8E3" rx="2"/>
+ <rect width="26" height="4" x="15" y="58" fill="#FEE1D3" rx="2"/>
+ <rect width="14" height="4" x="42" y="18" fill="#E1DBF1" rx="2"/>
+ <rect width="14" height="4" x="14" y="38" fill="#6B4FBB" rx="2"/>
+ </g>
+ <g transform="translate(3 67)">
+ <path fill="#FFFFFF" fill-rule="nonzero" d="M126.028,112 L150,112 C169.972,111.457 186,95.1 186,75 C186,54.565 169.435,38 149,38 C148.007,38 147.023,38.04 146.05,38.116 C141.1,23.51 127.277,13 111,13 C105.71511,12.9924557 100.490375,14.1212986 95.68,16.31 C89.03,6.47 77.77,0 65,0 C44.565,0 28,16.565 28,37 C28,37.7 28.02,38.397 28.058,39.088 C11.944,43.088 0,57.648 0,75 C0,95.435 16.565,112 37,112 C37.324,112 37.646,111.996 37.968,111.988 L126.028,112 Z"/>
+ <path fill="#FFFFFF" d="M126.028,110 L149.946,110 C168.876,109.486 184,93.976 184,75 C184,55.67 168.33,40 149,40 C148.064,40 147.133,40.037 146.207,40.11 L144.656,40.232 L144.156,38.758 C139.381,24.67 126.116,15 111,15 C106.001056,14.9926759 101.058995,16.0604828 96.509,18.131 L94.969,18.832 L94.022,17.431 C87.552,7.855 76.774,2 65,2 C45.67,2 30,17.67 30,37 C30,37.661 30.018,38.32 30.055,38.977 L30.147,40.63 L28.54,41.029 C13.06,44.87 2,58.827 2,75 C2,94.33 17.67,110 37,110 C37.306,110 37.612,109.996 37.968,109.988 L126.028,110 Z"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M149,38 C169.434569,38 186,54.5654305 186,75 C186,95.0477955 170.024944,111.45554 149.946,112 L126.028,112 L38.0129325,111.987495 C37.6348039,111.995992 37.3157048,112 37,112 C16.5654305,112 0,95.4345695 0,75 C0,57.9772494 11.6188339,43.1669444 28.0580557,39.0879359 C28.0192558,38.39805 28,37.7022134 28,37 C28,16.5654305 44.5654305,0 65,0 C77.3556804,0 88.7905015,6.1156181 95.6789703,16.310978 C100.491636,14.1213221 105.717209,12.9922579 111,13 C126.893041,13 140.974134,23.139867 146.049999,38.1155308 C147.031471,38.0388091 148.014765,38 149,38 Z M149.891715,108.000737 C167.750695,107.515818 182,92.8805667 182,75 C182,56.7745695 167.225431,42 149,42 C148.1197,42 147.241142,42.0346799 146.363833,42.1038413 L144.812833,42.2258413 C143.900567,42.2975992 143.055957,41.7410534 142.762001,40.8744692 L142.261844,39.400007 C137.735045,26.0442906 125.175364,17 110.99707,16.9999979 C106.284902,16.9930939 101.626355,17.9996435 97.3375854,19.9512874 L95.7975854,20.6522874 C94.9091924,21.0566793 93.8586575,20.7607079 93.3120297,19.952022 L92.3648004,18.5506827 C86.2175802,9.45241684 76.0227954,4 65,4 C46.7745695,4 32,18.7745695 32,37 C32,37.6282966 32.0172077,38.249659 32.0519095,38.8658592 L32.1439095,40.5188592 C32.1972908,41.4779816 31.5612439,42.3395846 30.6289443,42.5710641 L29.0216479,42.9701376 C14.3638424,46.6071293 4,59.8177208 4,75 C4,93.2254305 18.7745695,108 37,108 C37.2837952,108 37.5733598,107.996363 37.9682725,107.988 L126.02825,108 L149.891715,108.000737 Z"/>
+ </g>
+ <g fill-rule="nonzero" transform="rotate(15 -315.035 277.714)">
+ <path fill="#FFFFFF" d="M12.275,10.57 C13.986216,9.15630755 15.921048,8.03765363 18,7.26 L18,5.5 C18,2.463 20.47,0 23.493,0 L26.507,0 C27.9648848,0.000530018716 29.3628038,0.580386367 30.3930274,1.61192286 C31.4232511,2.64345935 32.0013267,4.04211574 32,5.5 L32,7.26 C34.098,8.043 36.03,9.17 37.725,10.57 L39.253,9.688 C41.8816141,8.17268496 45.2407537,9.07039379 46.763,11.695 L48.27,14.305 C48.9984289,15.5678669 49.1951495,17.0684426 48.8168566,18.4763972 C48.4385638,19.8843518 47.5162683,21.0842673 46.253,21.812 L44.728,22.693 C44.907,23.769 45,24.873 45,26 C45,27.127 44.907,28.231 44.728,29.307 L46.253,30.187 C48.8800379,31.705769 49.7822744,35.0642181 48.27,37.695 L46.763,40.305 C46.0335844,41.5673849 44.8323832,42.4881439 43.4238487,42.8645658 C42.0153143,43.2409877 40.5149245,43.0422119 39.253,42.312 L37.725,41.43 C36.013784,42.8436924 34.078952,43.9623464 32,44.74 L32,46.5 C32,49.537 29.53,52 26.507,52 L23.493,52 C22.0351152,51.99947 20.6371962,51.4196136 19.6069726,50.3880771 C18.5767489,49.3565406 17.9986733,47.9578843 18,46.5 L18,44.74 C15.921048,43.9623464 13.986216,42.8436924 12.275,41.43 L10.747,42.312 C8.11838594,43.827315 4.75924629,42.9296062 3.237,40.305 L1.73,37.695 C1.00157113,36.4321331 0.804850523,34.9315574 1.18314337,33.5236028 C1.56143621,32.1156482 2.48373172,30.9157327 3.747,30.188 L5.272,29.307 C5.09051204,28.2140265 4.9995366,27.107939 5,26 C5,24.873 5.093,23.769 5.272,22.693 L3.747,21.813 C1.11996213,20.294231 0.217725591,16.9357819 1.73,14.305 L3.237,11.695 C3.96641559,10.4326151 5.16761682,9.51185609 6.57615125,9.13543417 C7.98468568,8.75901226 9.48507553,8.95778814 10.747,9.688 L12.275,10.57 Z"/>
+ <path fill="#E1DBF1" d="M17.9996486,7.25963195 L18.0000013,5.49772675 C18.0034459,2.46713881 20.4561478,0.00952173148 23.493,0 L26.507,0 C29.542757,0 32,2.46161709 32,5.5 L32,7.25850184 C34.0799663,8.03664754 36.0149544,9.15559094 37.7260175,10.5694605 L39.2547869,9.68691874 C41.8812087,8.17416302 45.2363972,9.06948854 46.7630175,11.6949424 L48.270687,14.3061027 C48.9989901,15.569417 49.1952874,17.0704122 48.816349,18.4785295 C48.4374106,19.8866468 47.5143145,21.0864021 46.2530682,21.8120114 L44.7278655,22.6926677 C44.9091017,23.7802451 45,24.8850821 45,26 C45,27.1144218 44.9091826,28.218078 44.7278653,29.3073326 L46.2547984,30.1889888 C48.8778516,31.7070439 49.7801588,35.0599752 48.2700175,37.6950576 L46.7625317,40.3058986 C46.0327098,41.5684739 44.8309328,42.4891542 43.4219037,42.8651509 C42.0128746,43.2411475 40.512172,43.0416186 39.2533538,42.312255 L37.7244858,41.4299789 C36.013753,42.8435912 34.0794396,43.9622923 32.0003514,44.7403681 L31.9999987,46.5022733 C31.9965541,49.5328612 29.5438522,51.9904783 26.507,52 L23.493,52 C20.457243,52 18,49.5383829 18,46.5 L18,44.7414988 C15.9200337,43.9633525 13.9850456,42.8444091 12.2739825,41.4305395 L10.7452131,42.3130813 C8.11879127,43.825837 4.76360277,42.9305115 3.23698247,40.3050576 L1.72931303,37.6938973 C1.0010099,36.430583 0.804712603,34.9295878 1.18365098,33.5214705 C1.56258936,32.1133532 2.48568546,30.9135979 3.74693178,30.1879886 L5.27213454,29.3073323 C5.09089825,28.2197549 5,27.114918 5.00000019,26.0008761 C4.99951488,24.8930059 5.0904571,23.7869854 5.27213502,22.6926675 L3.74520157,21.8110112 C1.12214836,20.2929561 0.219841192,16.9400248 1.72998247,14.3049424 L3.23746831,11.6941014 C3.96729024,10.4315261 5.16906725,9.51084579 6.5780963,9.13484913 C7.98712536,8.75885247 9.48782803,8.95838137 10.7466462,9.687745 L12.2748018,10.56961 C14.0209791,9.13635584 15.9392199,8.03072455 17.9996486,7.25963195 Z M13.7518374,14.537862 C13.108069,15.069723 12.2016163,15.1456339 11.4783538,14.728255 L8.74433999,13.1505123 C8.40103903,12.9516035 7.99274958,12.8973186 7.60940137,12.9996143 C7.22605315,13.10191 6.89909107,13.3523954 6.70101753,13.6950576 L5.19724591,16.2994454 C4.78547321,17.0179634 5.03203388,17.9341714 5.74706822,18.3479886 L8.47306822,19.9219886 C9.19530115,20.3390079 9.58295216,21.1604138 9.44574883,21.983032 L9.21798321,23.3486236 C9.07251948,24.2246212 8.99961081,25.111131 9,26 C9,26.8953847 9.0728258,27.7804297 9.21774883,28.649968 L9.44574883,30.016968 C9.58295216,30.8395862 9.19530115,31.6609921 8.47306822,32.0780114 L5.74435077,33.6535776 C5.40046982,33.851417 5.14932721,34.1778291 5.04623114,34.5609292 C4.94313508,34.9440294 4.9965408,35.3523984 5.19401753,35.6949424 L6.69795587,38.2996585 C7.11427713,39.0156351 8.03110189,39.260288 8.7470791,38.8479035 L11.4770791,37.2719035 C12.200376,36.8543519 13.1069795,36.9302031 13.7508374,37.462138 L14.8210499,38.3463136 C16.1898549,39.4774943 17.737648,40.3725891 19.3990866,40.9941596 L20.6990866,41.4791596 C21.4813437,41.7710017 22,42.5180761 22,43.353 L22,46.5 C22,47.3308348 22.6679761,48 23.493,48 L26.5007228,48.0000099 C27.328845,47.9974107 27.99906,47.3258525 28,46.5 L28,43.353 C28,42.5185702 28.5180515,41.771829 29.2996486,41.4796319 L30.599003,40.9938734 C32.261836,40.3715765 33.8093225,39.4764853 35.1790197,38.3444304 L36.2490197,37.4614304 C36.8927697,36.9301861 37.798736,36.8545694 38.5216462,37.271745 L41.25566,38.8494877 C41.598961,39.0483965 42.0072504,39.1026814 42.3905986,39.0003857 C42.7739468,38.89809 43.1009089,38.6476046 43.2989825,38.3049424 L44.8027541,35.7005546 C45.2145268,34.9820366 44.9679661,34.0658286 44.2529318,33.6520114 L41.5269318,32.0780114 C40.8046988,31.6609921 40.4170478,30.8395862 40.5542512,30.016968 L40.7821577,28.6505288 C40.9272286,27.7792134 41,26.8950523 41,26 C41,25.1046153 40.9271742,24.2195703 40.7822512,23.350032 L40.5542512,21.983032 C40.4170478,21.1604138 40.8046988,20.3390079 41.5269318,19.9219886 L44.2556492,18.3464224 C44.5995302,18.148583 44.8506728,17.8221709 44.9537689,17.4390708 C45.0568649,17.0559706 45.0034592,16.6476016 44.8059825,16.3050576 L43.3020441,13.7003415 C42.8857229,12.9843649 41.9688981,12.739712 41.2529209,13.1520965 L38.5229209,14.7280965 C37.799624,15.1456481 36.8930205,15.0697969 36.2491626,14.537862 L35.1789501,13.6536864 C33.8101451,12.5225057 32.262352,11.6274109 30.6021792,11.0063122 L29.3021792,10.5223122 C28.5192618,10.230826 28,9.48341836 28,8.648 L28,5.5 C28,4.66916515 27.3320239,4 26.507,4 L23.4992772,3.99999015 C22.671155,4.00258933 22.00094,4.67414748 22,5.5 L22,8.647 C22,9.48142977 21.4819485,10.228171 20.7003514,10.5203681 L19.400997,11.0061266 C17.738164,11.6284235 16.1906775,12.5235147 14.822142,13.6546103 C14.8121128,13.6628994 14.4553446,13.9573166 13.7518374,14.537862 Z"/>
+ <g transform="rotate(15 -59.137 82.348)">
+ <circle cx="8" cy="8" r="8" fill="#FFFFFF" transform="translate(.035 6.008)"/>
+ <path fill="#6B4FBB" d="M7.40192379,14.7679492 C2.98364579,14.7679492 -0.598076211,11.1862272 -0.598076211,6.76794919 C-0.598076211,2.34967119 2.98364579,-1.23205081 7.40192379,-1.23205081 C11.8202018,-1.23205081 15.4019238,2.34967119 15.4019238,6.76794919 C15.4019238,11.1862272 11.8202018,14.7679492 7.40192379,14.7679492 Z M7.40192379,10.7679492 C9.61106279,10.7679492 11.4019238,8.97708819 11.4019238,6.76794919 C11.4019238,4.55881019 9.61106279,2.76794919 7.40192379,2.76794919 C5.19278479,2.76794919 3.40192379,4.55881019 3.40192379,6.76794919 C3.40192379,8.97708819 5.19278479,10.7679492 7.40192379,10.7679492 Z"/>
+ </g>
+ </g>
+ <g fill-rule="nonzero" transform="rotate(15 -402.968 460.884)">
+ <path fill="#FFFFFF" d="M9.82,8.53730769 C11.1889728,7.39547918 12.7368384,6.49195101 14.4,5.86384615 L14.4,4.44230769 C14.4,1.98934615 16.376,0 18.7944,0 L21.2056,0 C22.3719078,0.00042809204 23.4902431,0.468773604 24.314422,1.30193769 C25.1386009,2.13510179 25.6010613,3.26478579 25.6,4.44230769 L25.6,5.86384615 C27.2784,6.49626923 28.824,7.40653846 30.18,8.53730769 L31.4024,7.82492308 C33.5052912,6.60101478 36.192603,7.32608729 37.4104,9.44596154 L38.616,11.5540385 C39.1987431,12.5740464 39.3561196,13.7860498 39.0534853,14.9232439 C38.750851,16.060438 38.0130146,17.0296006 37.0024,17.6173846 L35.7824,18.3289615 C35.9256,19.1980385 36,20.0897308 36,21 C36,21.9102692 35.9256,22.8019615 35.7824,23.6710385 L37.0024,24.3818077 C39.1040303,25.6085057 39.8258195,28.3210992 38.616,30.4459615 L37.4104,32.5540385 C36.8268675,33.573657 35.8659065,34.317347 34.739079,34.6213801 C33.6122515,34.9254132 32.4119396,34.7648634 31.4024,34.1750769 L30.18,33.4626923 C28.8110272,34.6045208 27.2631616,35.508049 25.6,36.1361538 L25.6,37.5576923 C25.6,40.0106538 23.624,42 21.2056,42 L18.7944,42 C17.6280922,41.9995719 16.5097569,41.5312264 15.685578,40.6980623 C14.8613991,39.8648982 14.3989387,38.7352142 14.4,37.5576923 L14.4,36.1361538 C12.7368384,35.508049 11.1889728,34.6045208 9.82,33.4626923 L8.5976,34.1750769 C6.49470875,35.3989852 3.80739703,34.6739127 2.5896,32.5540385 L1.384,30.4459615 C0.8012569,29.4259536 0.643880418,28.2139502 0.946514692,27.0767561 C1.24914897,25.939562 1.98698538,24.9703994 2.9976,24.3826154 L4.2176,23.6710385 C4.07240963,22.7882521 3.99962928,21.8948738 4,21 C4,20.0897308 4.0744,19.1980385 4.2176,18.3289615 L2.9976,17.6181923 C0.895969702,16.3914943 0.174180473,13.6789008 1.384,11.5540385 L2.5896,9.44596154 C3.17313247,8.42634297 4.13409345,7.682653 5.260921,7.37861991 C6.38774855,7.07458682 7.58806043,7.23513658 8.5976,7.82492308 L9.82,8.53730769 Z"/>
+ <path fill="#FEE1D3" d="M14.0000007,5.6038043 L14.0000013,4.44005609 C14.0029906,1.78475013 16.1390906,-0.376211234 18.7944,-0.384615385 L21.2056,-0.384615385 C23.8595941,-0.384615385 26,1.78021801 26,4.44230769 L26,5.60295806 C27.5208716,6.20655954 28.9434678,7.03621848 30.2204219,8.06411282 L31.1970056,7.49492104 C33.4941909,6.15907529 36.4301298,6.95005805 37.7609369,9.26076474 L38.9671983,11.3699991 C39.5988409,12.4761812 39.768854,13.7886936 39.4405746,15.0202941 C39.1116282,16.2543969 38.308799,17.3078735 37.2096539,17.946304 L36.2175721,18.5246428 C36.3390841,19.3401617 36.4,20.1667594 36.4,21 C36.4,21.8329668 36.339124,22.6588262 36.2175401,23.4753391 L37.2113882,24.0547082 C39.4944154,25.3886826 40.276605,28.3232105 38.9665369,30.6311583 L37.7604568,32.7400742 C37.1252608,33.8495148 36.0768547,34.6604208 34.8452776,34.9922248 C33.6111681,35.324711 32.2964469,35.1482289 31.195569,34.5042428 L30.2192355,33.9354047 C28.9426535,34.9630196 27.5206806,35.7924453 25.9999993,36.3961957 L25.9999987,37.5599439 C25.9970094,40.2152499 23.8609094,42.3762112 21.2056,42.3846154 L18.7944,42.3846154 C16.1404059,42.3846154 14,40.219782 14,37.5576923 L14,36.3970419 C12.4791284,35.7934405 11.0565322,34.9637815 9.77957815,33.9358872 L8.80299442,34.505079 C6.50580915,35.8409247 3.56987021,35.049942 2.23906313,32.7392353 L1.03280169,30.6300009 C0.401159146,29.5238188 0.231145999,28.2113064 0.559425405,26.9797059 C0.888371786,25.7456031 1.69120101,24.6921265 2.79034606,24.053696 L3.78242779,23.4753573 C3.66091587,22.6598457 3.60000002,21.8333228 3.60000019,21.0008678 C3.59964068,20.1722851 3.66061719,19.3449468 3.78254167,18.5247085 L2.78861183,17.9452918 C0.505584602,16.6113174 -0.276605002,13.6767895 1.03346313,11.3688417 L2.23954317,9.25992583 C2.87473915,8.15048519 3.92314533,7.33957919 5.15472238,7.00777521 C6.38883187,6.67528896 7.70355311,6.85177112 8.80443097,7.49575721 L9.78076186,8.06459377 C11.0573465,7.03698045 12.4793194,6.20755475 14.0000007,5.6038043 Z M11.2634746,12.0326234 C10.617233,12.5716613 9.7026973,12.6485026 8.97556903,12.2248582 L6.78774825,10.9501716 C6.60754053,10.8447551 6.39506809,10.8162338 6.19527576,10.8700606 C5.99295099,10.9245697 5.8183659,11.0596053 5.71133687,11.246543 L4.50892658,13.3490215 C4.28085652,13.7508163 4.41776119,14.2644394 4.80485394,14.4906191 L6.98565394,15.7619268 C7.70254629,16.1798426 8.08690703,16.9970357 7.95165511,17.8157512 L7.76948523,18.9184706 C7.65638664,19.6061109 7.59969735,20.3020342 7.6,21 C7.6,21.7031066 7.65662064,22.3978283 7.76925511,23.0801334 L7.95165511,24.1842488 C8.08690703,25.0029643 7.70254629,25.8201574 6.98565394,26.2380732 L4.80213007,27.5109659 C4.61772321,27.6180778 4.48116147,27.7972748 4.42448029,28.0099246 C4.36713215,28.2250767 4.39688141,28.454743 4.50573687,28.6453801 L5.70831165,30.7481858 C5.93243371,31.1373303 6.41410538,31.2670993 6.79049373,31.0482253 L8.97449373,29.7753023 C9.7016554,29.3514832 10.6163433,29.4282639 11.2626746,29.9673766 L12.1188867,30.6815536 C13.1796505,31.566598 14.3786867,32.2666727 15.6649769,32.7525215 L16.7049769,33.1442523 C17.4841581,33.4377419 18,34.1832625 18,35.0158846 L18,37.5576923 C18,38.02074 18.3597694,38.3846154 18.7944,38.3846154 L21.1992624,38.3846254 C21.6372484,38.3832375 21.9994819,38.0167881 22,37.5576923 L22,35.0158846 C22,34.18376 22.5152346,33.4385758 23.2937506,33.1447321 L24.3331012,32.7524389 C25.620867,32.2658727 26.8196661,31.5658006 27.8813806,30.679856 L28.7373806,29.9666637 C29.3836087,29.4282468 30.2976553,29.3517028 31.024431,29.7751418 L33.2122517,31.0498284 C33.3924595,31.1552449 33.6049319,31.1837662 33.8047242,31.1299394 C34.007049,31.0754303 34.1816341,30.9403947 34.2886631,30.753457 L35.4910734,28.6509785 C35.7191435,28.2491837 35.5822388,27.7355606 35.1951461,27.5093809 L33.0143461,26.2380732 C32.2974537,25.8201574 31.913093,25.0029643 32.0483449,24.1842488 L32.2306531,23.0806893 C32.3434217,22.3968737 32.4,21.7028459 32.4,21 C32.4,20.2968934 32.3433794,19.6021717 32.2307449,18.9198666 L32.0483449,17.8157512 C31.913093,16.9970357 32.2974537,16.1798426 33.0143461,15.7619268 L35.1978699,14.4890341 C35.3822768,14.3819222 35.5188385,14.2027252 35.5755197,13.9900754 C35.6328679,13.7749233 35.6031186,13.545257 35.4942631,13.3546199 L34.2916883,11.2518142 C34.0675663,10.8626697 33.5858946,10.7329007 33.2095063,10.9517747 L31.0255063,12.2246977 C30.2983446,12.6485168 29.3836567,12.5717361 28.7373254,12.0326234 L27.8811133,11.3184464 C26.8203495,10.433402 25.6213133,9.73332732 24.3362966,9.24795765 L23.2962966,8.85703457 C22.5164499,8.56389992 22,7.81804293 22,6.98492308 L22,4.44230769 C22,3.97925995 21.6402306,3.61538462 21.2056,3.61538462 L18.8007376,3.61537457 C18.3627516,3.61676247 18.0005181,3.98321188 18,4.44230769 L18,6.98411538 C18,7.81623999 17.4847654,8.56142419 16.7062494,8.85526793 L15.6668988,9.24756113 C14.379133,9.73412728 13.1803339,10.4341994 12.1197785,11.3191775 C12.1108094,11.3266617 11.8253748,11.564477 11.2634746,12.0326234 Z"/>
+ <g transform="rotate(15 -47.892 66.043)">
+ <ellipse cx="6.4" cy="6.462" fill="#FFFFFF" rx="6.4" ry="6.462" transform="translate(.028 4.853)"/>
+ <path fill="#FC6D26" d="M5.92153903,11.9125743 C2.3834711,11.9125743 -0.478460969,9.0231237 -0.478460969,5.4664205 C-0.478460969,1.9097173 2.3834711,-0.979733345 5.92153903,-0.979733345 C9.45960696,-0.979733345 12.321539,1.9097173 12.321539,5.4664205 C12.321539,9.0231237 9.45960696,11.9125743 5.92153903,11.9125743 Z M5.92153903,8.71257435 C7.6854047,8.71257435 9.12153903,7.26263103 9.12153903,5.4664205 C9.12153903,3.67020997 7.6854047,2.22026666 5.92153903,2.22026666 C4.15767337,2.22026666 2.72153903,3.67020997 2.72153903,5.4664205 C2.72153903,7.26263103 4.15767337,8.71257435 5.92153903,8.71257435 Z"/>
+ </g>
+ </g>
+ <path fill="#000000" fill-opacity=".03" d="M61.6792606,38.251778 C61.8904713,36.8653316 62,35.4454567 62,34 C62,18.536027 49.463973,6 34,6 C18.536027,6 6,18.536027 6,34 C6,49.463973 18.536027,62 34,62 C42.8132237,62 50.6754255,57.9281916 55.8080076,51.5631726 L64.2689981,50.6250607 C64.4699867,50.6027761 64.6664333,50.5501384 64.8516368,50.4689431 C65.8632575,50.0254374 66.3238058,48.8458244 65.8803001,47.8342037 L65.8803001,47.8342037 L61.6792599,38.2517794 Z"/>
+ <path fill="#FFFFFF" d="M63.6792606,34.251778 C63.8904713,32.8653316 64,31.4454567 64,30 C64,14.536027 51.463973,2 36,2 C20.536027,2 8,14.536027 8,30 C8,45.463973 20.536027,58 36,58 C44.8132237,58 52.6754255,53.9281916 57.8080076,47.5631726 L66.2689981,46.6250607 C66.4699867,46.6027761 66.6664333,46.5501384 66.8516368,46.4689431 C67.8632575,46.0254374 68.3238058,44.8458244 67.8803001,43.8342037 L67.8803001,43.8342037 L63.6792599,34.2517794 Z"/>
+ <path fill="#EEEEEE" fill-rule="nonzero" d="M69.7120015,43.0311656 C70.5990128,45.0544071 69.6779163,47.4136331 67.6546748,48.3006445 C67.2842678,48.463035 66.8913746,48.5683104 66.4893975,48.6128796 L58.8313193,49.4619687 C53.1777737,56.0908093 44.9077957,60 36,60 C19.4314575,60 6,46.5685425 6,30 C6,13.4314575 19.4314575,0 36,0 C52.5685425,0 66,13.4314575 66,30 C66,31.335699 65.9125851,32.6609639 65.739427,33.9698636 L69.7120015,43.0311656 Z M61.7020717,33.9505738 C61.8999153,32.6518726 62,31.332589 62,30 C62,15.6405965 50.3594035,4 36,4 C21.6405965,4 10,15.6405965 10,30 C10,44.3594035 21.6405965,56 36,56 C43.969518,56 51.3430155,52.3943837 56.251122,46.3077415 L56.7684631,45.6661764 L66.0485988,44.6372417 L61.8475593,35.054816 L61.6147491,34.5237842 L61.7020717,33.9505738 Z"/>
+ <g fill="#31AF64" fill-rule="nonzero" transform="translate(24 18)">
+ <path d="M12.5,26.5 C4.7680135,26.5 -1.5,20.2319865 -1.5,12.5 C-1.5,4.7680135 4.7680135,-1.5 12.5,-1.5 C20.2319865,-1.5 26.5,4.7680135 26.5,12.5 C26.5,20.2319865 20.2319865,26.5 12.5,26.5 Z M12.5,23.5 C18.5751322,23.5 23.5,18.5751322 23.5,12.5 C23.5,6.42486775 18.5751322,1.5 12.5,1.5 C6.42486775,1.5 1.5,6.42486775 1.5,12.5 C1.5,18.5751322 6.42486775,23.5 12.5,23.5 Z"/>
+ <path d="M11.18,13.81 L9.248,11.878 C8.67483243,11.3054203 7.74616757,11.3054203 7.173,11.878 C6.89709997,12.1525667 6.74198837,12.5257601 6.74198837,12.915 C6.74198837,13.3042399 6.89709997,13.6774333 7.173,13.952 L10.048,16.826 C10.0636337,16.8423622 10.0796378,16.8583663 10.096,16.874 C10.646,17.424 11.526,17.423 12.071,16.879 L17.879,11.071 C18.1403709,10.8085057 18.2866977,10.4528922 18.2857599,10.0824639 C18.2848221,9.71203558 18.1366966,9.35716757 17.874,9.096 C17.6132271,8.83256594 17.2582132,8.68392968 16.8875393,8.68299126 C16.5168653,8.68205285 16.1611034,8.82888967 15.899,9.091 L11.18,13.81 Z"/>
+ </g>
+ </g>
+</svg>
diff --git a/app/workers/git_garbage_collect_worker.rb b/app/workers/git_garbage_collect_worker.rb
index c95497dfaba..ec65d3ff65e 100644
--- a/app/workers/git_garbage_collect_worker.rb
+++ b/app/workers/git_garbage_collect_worker.rb
@@ -5,6 +5,9 @@ class GitGarbageCollectWorker
sidekiq_options retry: false
+ # Timeout set to 24h
+ LEASE_TIMEOUT = 86400
+
GITALY_MIGRATED_TASKS = {
gc: :garbage_collect,
full_repack: :repack_full,
@@ -13,8 +16,19 @@ class GitGarbageCollectWorker
def perform(project_id, task = :gc, lease_key = nil, lease_uuid = nil)
project = Project.find(project_id)
- task = task.to_sym
+ active_uuid = get_lease_uuid(lease_key)
+
+ if active_uuid
+ return unless active_uuid == lease_uuid
+
+ renew_lease(lease_key, active_uuid)
+ else
+ lease_uuid = try_obtain_lease(lease_key)
+
+ return unless lease_uuid
+ end
+ task = task.to_sym
cmd = command(task)
repo_path = project.repository.path_to_repo
description = "'#{cmd.join(' ')}' in #{repo_path}"
@@ -33,11 +47,27 @@ class GitGarbageCollectWorker
# Refresh the branch cache in case garbage collection caused a ref lookup to fail
flush_ref_caches(project) if task == :gc
ensure
- Gitlab::ExclusiveLease.cancel(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
+ cancel_lease(lease_key, lease_uuid) if lease_key.present? && lease_uuid.present?
end
private
+ def try_obtain_lease(key)
+ ::Gitlab::ExclusiveLease.new(key, timeout: LEASE_TIMEOUT).try_obtain
+ end
+
+ def renew_lease(key, uuid)
+ ::Gitlab::ExclusiveLease.new(key, uuid: uuid, timeout: LEASE_TIMEOUT).renew
+ end
+
+ def cancel_lease(key, uuid)
+ ::Gitlab::ExclusiveLease.cancel(key, uuid)
+ end
+
+ def get_lease_uuid(key)
+ ::Gitlab::ExclusiveLease.get_uuid(key)
+ end
+
## `repository` has to be a Gitlab::Git::Repository
def gitaly_call(task, repository)
client = Gitlab::GitalyClient::RepositoryService.new(repository)
diff --git a/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml
new file mode 100644
index 00000000000..abb9e33d626
--- /dev/null
+++ b/changelogs/unreleased/23079-remove-default-scope-in-sortable.yml
@@ -0,0 +1,5 @@
+---
+title: Removes Sortable default scope.
+merge_request: 13558
+author:
+type: fixed
diff --git a/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml
new file mode 100644
index 00000000000..4a8d8097169
--- /dev/null
+++ b/changelogs/unreleased/31362_decrease_cyclomatic_complexity_threshold_step3.yml
@@ -0,0 +1,5 @@
+---
+title: Decrease Cyclomatic Complexity threshold to 14
+merge_request: 13972
+author: Maxim Rydkin
+type: other
diff --git a/changelogs/unreleased/32665-refactor-project-visibility-settings.yml b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml
new file mode 100644
index 00000000000..fde70c47ca6
--- /dev/null
+++ b/changelogs/unreleased/32665-refactor-project-visibility-settings.yml
@@ -0,0 +1,5 @@
+---
+title: Redesign project feature permissions settings
+merge_request: 14062
+author:
+type: changed
diff --git a/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
new file mode 100644
index 00000000000..74aa337a18c
--- /dev/null
+++ b/changelogs/unreleased/35012-navigation-add-option-to-change-navigation-color-palette.yml
@@ -0,0 +1,5 @@
+---
+title: Add option in preferences to change navigation theme color
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml
new file mode 100644
index 00000000000..76c9c905590
--- /dev/null
+++ b/changelogs/unreleased/36638-select-project-to-create-issue-button-is-disconnected-from-dropdown-button.yml
@@ -0,0 +1,5 @@
+---
+title: Normalize styles for empty state combo button
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/37158-autodevops-banner.yml b/changelogs/unreleased/37158-autodevops-banner.yml
new file mode 100644
index 00000000000..f0ffb670102
--- /dev/null
+++ b/changelogs/unreleased/37158-autodevops-banner.yml
@@ -0,0 +1,5 @@
+---
+title: Created callout for auto devops
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml
new file mode 100644
index 00000000000..d6d21ac4c51
--- /dev/null
+++ b/changelogs/unreleased/37288-fix-wrong-header-when-testing-webhook.yml
@@ -0,0 +1,5 @@
+---
+title: Fix a wrong `X-Gitlab-Event` header when testing webhooks
+merge_request: 14108
+author:
+type: fixed
diff --git a/changelogs/unreleased/37368-blob-viewer-on-mobile.yml b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml
new file mode 100644
index 00000000000..6a955f5715f
--- /dev/null
+++ b/changelogs/unreleased/37368-blob-viewer-on-mobile.yml
@@ -0,0 +1,5 @@
+---
+title: Make blob viewer for rich contents wider for mobile
+merge_request: 14011
+author: Takuya Noguchi
+type: fixed
diff --git a/changelogs/unreleased/docs-confidential-issue.yml b/changelogs/unreleased/docs-confidential-issue.yml
new file mode 100644
index 00000000000..841970ef4cf
--- /dev/null
+++ b/changelogs/unreleased/docs-confidential-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Update documentation for confidential issue
+merge_request: 14117
+author:
+type: other
diff --git a/changelogs/unreleased/fix-gem-security-updates.yml b/changelogs/unreleased/fix-gem-security-updates.yml
deleted file mode 100644
index dce11d08402..00000000000
--- a/changelogs/unreleased/fix-gem-security-updates.yml
+++ /dev/null
@@ -1,5 +0,0 @@
----
-title: Upgrade mail and nokogiri gems due to security issues
-merge_request: 13662
-author: Markus Koller
-type: security
diff --git a/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml
new file mode 100644
index 00000000000..90c25d782c8
--- /dev/null
+++ b/changelogs/unreleased/gitaly-feature-toggles-development-opt-out.yml
@@ -0,0 +1,5 @@
+---
+title: Gitaly feature toggles are on by default in development
+merge_request: 13802
+author:
+type: other
diff --git a/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml
new file mode 100644
index 00000000000..ec0d1b245e4
--- /dev/null
+++ b/changelogs/unreleased/improve-share-locking-feature-for-subgroups.yml
@@ -0,0 +1,6 @@
+---
+title: '"Share with group lock" now applies to subgroups, but owner can override setting
+ on subgroups'
+merge_request: 13944
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-add-grape-logging.yml b/changelogs/unreleased/sh-add-grape-logging.yml
new file mode 100644
index 00000000000..eaf6cb045d5
--- /dev/null
+++ b/changelogs/unreleased/sh-add-grape-logging.yml
@@ -0,0 +1,5 @@
+---
+title: Add JSON logger in `log/api_json.log` for Grape API endpoints
+merge_request:
+author:
+type: added
diff --git a/changelogs/unreleased/support-additional-colors.yml b/changelogs/unreleased/support-additional-colors.yml
new file mode 100644
index 00000000000..5178e159dcf
--- /dev/null
+++ b/changelogs/unreleased/support-additional-colors.yml
@@ -0,0 +1,5 @@
+---
+title: Added support for specific labels and colors
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/zj-auto-devops-table.yml b/changelogs/unreleased/zj-auto-devops-table.yml
new file mode 100644
index 00000000000..f1a004ebd19
--- /dev/null
+++ b/changelogs/unreleased/zj-auto-devops-table.yml
@@ -0,0 +1,5 @@
+---
+title: Allow users and administrator to configure Auto-DevOps
+merge_request: 13923
+author:
+type: added
diff --git a/changelogs/unreleased/zj-update-rails-template.yml b/changelogs/unreleased/zj-update-rails-template.yml
new file mode 100644
index 00000000000..5464f0e3d42
--- /dev/null
+++ b/changelogs/unreleased/zj-update-rails-template.yml
@@ -0,0 +1,5 @@
+---
+title: Update Rails project template to use Postgresql by default
+merge_request:
+author:
+type: changed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index e9661090844..cd44f888d3f 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -76,6 +76,13 @@ production: &base
# default_can_create_group: false # default: true
# username_changing_enabled: false # default: true - User can change her username/namespace
+ ## Default theme ID
+ ## 1 - Indigo
+ ## 2 - Dark
+ ## 3 - Light
+ ## 4 - Blue
+ ## 5 - Green
+ # default_theme: 1 # default: 1
## Automatic issue closing
# If a commit message matches this regular expression, all issues referenced from the matched text will be closed.
diff --git a/config/initializers/0_inflections.rb b/config/initializers/0_inflections.rb
index f977104ff9d..1ad9ddca877 100644
--- a/config/initializers/0_inflections.rb
+++ b/config/initializers/0_inflections.rb
@@ -10,5 +10,10 @@
# end
#
ActiveSupport::Inflector.inflections do |inflect|
- inflect.uncountable %w(award_emoji project_statistics system_note_metadata)
+ inflect.uncountable %w(
+ award_emoji
+ project_statistics
+ system_note_metadata
+ project_auto_devops
+ )
end
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 7c1ca05a57b..40fbdd3ef9b 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -232,6 +232,7 @@ Settings['gitlab'] ||= Settingslogic.new({})
Settings.gitlab['default_projects_limit'] ||= 100000
Settings.gitlab['default_branch_protection'] ||= 2
Settings.gitlab['default_can_create_group'] = true if Settings.gitlab['default_can_create_group'].nil?
+Settings.gitlab['default_theme'] = Gitlab::Themes::APPLICATION_DEFAULT if Settings.gitlab['default_theme'].nil?
Settings.gitlab['host'] ||= ENV['GITLAB_HOST'] || 'localhost'
Settings.gitlab['ssh_host'] ||= Settings.gitlab.host
Settings.gitlab['https'] = false if Settings.gitlab['https'].nil?
diff --git a/db/migrate/20170816234252_add_theme_id_to_users.rb b/db/migrate/20170816234252_add_theme_id_to_users.rb
new file mode 100644
index 00000000000..5043f9ec591
--- /dev/null
+++ b/db/migrate/20170816234252_add_theme_id_to_users.rb
@@ -0,0 +1,10 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddThemeIdToUsers < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column :users, :theme_id, :integer, limit: 2
+ end
+end
diff --git a/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb
new file mode 100644
index 00000000000..da518d8215c
--- /dev/null
+++ b/db/migrate/20170824101926_add_auto_devops_enabled_to_application_settings.rb
@@ -0,0 +1,15 @@
+class AddAutoDevopsEnabledToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:application_settings, :auto_devops_enabled, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:application_settings, :auto_devops_enabled, :boolean)
+ end
+end
diff --git a/db/migrate/20170828093725_create_project_auto_dev_ops.rb b/db/migrate/20170828093725_create_project_auto_dev_ops.rb
new file mode 100644
index 00000000000..c1bb4f20c1d
--- /dev/null
+++ b/db/migrate/20170828093725_create_project_auto_dev_ops.rb
@@ -0,0 +1,19 @@
+class CreateProjectAutoDevOps < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ create_table :project_auto_devops do |t|
+ t.belongs_to :project, null: false, index: { unique: true }, foreign_key: { on_delete: :cascade }
+ t.datetime_with_timezone :created_at, null: false
+ t.datetime_with_timezone :updated_at, null: false
+ t.boolean :enabled, default: nil, null: true
+ t.string :domain
+ end
+ end
+
+ def down
+ drop_table(:project_auto_devops)
+ end
+end
diff --git a/db/migrate/20170831092813_add_config_source_to_pipelines.rb b/db/migrate/20170831092813_add_config_source_to_pipelines.rb
new file mode 100644
index 00000000000..ff51e968abd
--- /dev/null
+++ b/db/migrate/20170831092813_add_config_source_to_pipelines.rb
@@ -0,0 +1,7 @@
+class AddConfigSourceToPipelines < ActiveRecord::Migration
+ DOWNTIME = false
+
+ def change
+ add_column(:ci_pipelines, :config_source, :integer, allow_null: true)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 04bdde8811b..2149f5ad23d 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -133,6 +133,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.integer "performance_bar_allowed_group_id"
t.boolean "hashed_storage_enabled", default: false, null: false
t.boolean "project_export_enabled", default: true, null: false
+ t.boolean "auto_devops_enabled", default: false, null: false
end
create_table "audit_events", force: :cascade do |t|
@@ -340,6 +341,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.integer "auto_canceled_by_id"
t.integer "pipeline_schedule_id"
t.integer "source"
+ t.integer "config_source"
t.boolean "protected"
end
@@ -1108,6 +1110,16 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_index "project_authorizations", ["project_id"], name: "index_project_authorizations_on_project_id", using: :btree
add_index "project_authorizations", ["user_id", "project_id", "access_level"], name: "index_project_authorizations_on_user_id_project_id_access_level", unique: true, using: :btree
+ create_table "project_auto_devops", force: :cascade do |t|
+ t.integer "project_id", null: false
+ t.datetime_with_timezone "created_at", null: false
+ t.datetime_with_timezone "updated_at", null: false
+ t.boolean "enabled"
+ t.string "domain"
+ end
+
+ add_index "project_auto_devops", ["project_id"], name: "index_project_auto_devops_on_project_id", unique: true, using: :btree
+
create_table "project_features", force: :cascade do |t|
t.integer "project_id"
t.integer "merge_requests_access_level"
@@ -1596,6 +1608,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
t.boolean "notified_of_own_activity"
t.string "preferred_language"
t.string "rss_token"
+ t.integer "theme_id", limit: 2
end
add_index "users", ["admin"], name: "index_users_on_admin", using: :btree
@@ -1699,7 +1712,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "issue_assignees", "users", name: "fk_5e0c8d9154", on_delete: :cascade
add_foreign_key "issue_metrics", "issues", on_delete: :cascade
add_foreign_key "issues", "projects", name: "fk_899c8f3231", on_delete: :cascade
- add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :cascade
+ add_foreign_key "issues", "users", column: "author_id", name: "fk_05f1e72feb", on_delete: :nullify
add_foreign_key "label_priorities", "labels", on_delete: :cascade
add_foreign_key "label_priorities", "projects", on_delete: :cascade
add_foreign_key "labels", "namespaces", column: "group_id", on_delete: :cascade
@@ -1723,6 +1736,7 @@ ActiveRecord::Schema.define(version: 20170905112933) do
add_foreign_key "personal_access_tokens", "users"
add_foreign_key "project_authorizations", "projects", on_delete: :cascade
add_foreign_key "project_authorizations", "users", on_delete: :cascade
+ add_foreign_key "project_auto_devops", "projects", on_delete: :cascade
add_foreign_key "project_features", "projects", name: "fk_18513d9b92", on_delete: :cascade
add_foreign_key "project_group_links", "projects", name: "fk_daa8cee94c", on_delete: :cascade
add_foreign_key "project_import_data", "projects", name: "fk_ffb9ee3a10", on_delete: :cascade
diff --git a/doc/README.md b/doc/README.md
index a59f71e83a5..b3e7c9bd0bf 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -24,7 +24,7 @@ plus premium features available in each version: **Enterprise Edition Starter**
Shortcuts to GitLab's most visited docs:
-| [GitLab CI](ci/README.md) | Other |
+| [GitLab CI/CD](ci/README.md) | Other |
| :----- | :----- |
| [Quick start guide](ci/quick_start/README.md) | [API](api/README.md) |
| [Configuring `.gitlab-ci.yml`](ci/yaml/README.md) | [SSH authentication](ssh/README.md) |
@@ -41,6 +41,7 @@ Shortcuts to GitLab's most visited docs:
- See also [GitLab Workflow - an overview](https://about.gitlab.com/2016/10/25/gitlab-workflow-an-overview/).
- [GitLab Markdown](user/markdown.md): GitLab's advanced formatting system (GitLab Flavored Markdown).
- [GitLab Quick Actions](user/project/quick_actions.md): Textual shortcuts for common actions on issues or merge requests that are usually done by clicking buttons or dropdowns in GitLab's UI.
+- [Auto DevOps](topics/autodevops/index.md)
### User account
diff --git a/doc/api/keys.md b/doc/api/keys.md
index 376ac27df3a..ddcf7830621 100644
--- a/doc/api/keys.md
+++ b/doc/api/keys.md
@@ -32,6 +32,7 @@ Parameters:
"twitter": "",
"website_url": "",
"email": "john@example.com",
+ "theme_id": 2,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": null,
diff --git a/doc/api/session.md b/doc/api/session.md
index f79eac11689..b97e26f34a2 100644
--- a/doc/api/session.md
+++ b/doc/api/session.md
@@ -39,6 +39,7 @@ Example response:
"twitter": "",
"website_url": "",
"email": "john@example.com",
+ "theme_id": 1,
"color_scheme_id": 1,
"projects_limit": 10,
"current_sign_in_at": "2015-07-07T07:10:58.392Z",
diff --git a/doc/api/users.md b/doc/api/users.md
index 9f3e4caf2f4..6d5db16b36a 100644
--- a/doc/api/users.md
+++ b/doc/api/users.md
@@ -72,6 +72,7 @@ GET /users
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -105,6 +106,7 @@ GET /users
"organization": "",
"last_sign_in_at": null,
"confirmed_at": "2012-05-30T16:53:06.148Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 3,
"projects_limit": 100,
@@ -215,6 +217,7 @@ Parameters:
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -341,6 +344,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
@@ -387,6 +391,7 @@ GET /user
"organization": "",
"last_sign_in_at": "2012-06-01T11:41:01Z",
"confirmed_at": "2012-05-23T09:05:22Z",
+ "theme_id": 1,
"last_activity_on": "2012-05-23",
"color_scheme_id": 2,
"projects_limit": 100,
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 1bf10e34ae7..5cfd82de381 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -44,6 +44,10 @@ digging into specific reference guides.
- [User permissions](../user/permissions.md#gitlab-ci)
- [Jobs permissions](../user/permissions.md#jobs-permissions)
+## Auto DevOps
+
+- [Auto DevOps](../topics/autodevops/index.md)
+
## GitLab CI + Docker
Leverage the power of Docker to run your CI pipelines.
diff --git a/doc/ci/autodeploy/index.md b/doc/ci/autodeploy/index.md
index a128cf69c20..474cb28b9e4 100644
--- a/doc/ci/autodeploy/index.md
+++ b/doc/ci/autodeploy/index.md
@@ -1,13 +1,16 @@
# Auto Deploy
->**Notes:**
-- [Introduced][mr-8135] in GitLab 8.15.
-- Auto deploy is an experimental feature and is not recommended for Production
- use at this time.
-- As of GitLab 9.1, access to the Container Registry is only available while
- the Pipeline is running. Restarting a pod, scaling a service, or other actions
- which require on-going access will fail. On-going secure access is planned for
- a subsequent release.
+> [Introduced][mr-8135] in GitLab 8.15.
+> Auto deploy is an experimental feature and is **not recommended for Production use** at this time.
+
+> As of GitLab 9.1, access to the container registry is only available while the
+Pipeline is running. Restarting a pod, scaling a service, or other actions which
+require on-going access **will fail**. On-going secure access is planned for a
+subsequent release.
+
+> As of GitLab 10.0, Auto Deploy templates are **deprecated** and the
+functionality has been included in [Auto
+DevOps](../../topics/autodevops/index.md).
Auto deploy is an easy way to configure GitLab CI for the deployment of your
application. GitLab Community maintains a list of `.gitlab-ci.yml`
@@ -122,4 +125,3 @@ If you have installed GitLab using a different method:
[kube-deploy]: https://gitlab.com/gitlab-examples/kubernetes-deploy "Kubernetes deploy example project"
[container-registry]: https://docs.gitlab.com/ce/user/project/container_registry.html
[postgresql]: https://www.postgresql.org/
-
diff --git a/doc/ci/environments.md b/doc/ci/environments.md
index cbf06afa294..c1362b7bd5b 100644
--- a/doc/ci/environments.md
+++ b/doc/ci/environments.md
@@ -446,8 +446,7 @@ and/or `production`) you can see this information in the merge request itself.
![Environment URLs in merge request](img/environments_link_url_mr.png)
-### <a name="route-map"></a>Go directly from source files to public pages on the environment
-
+### Go directly from source files to public pages on the environment
> Introduced in GitLab 8.17.
diff --git a/doc/development/img/manual_build_docs.png b/doc/development/img/manual_build_docs.png
new file mode 100644
index 00000000000..fef767c2a79
--- /dev/null
+++ b/doc/development/img/manual_build_docs.png
Binary files differ
diff --git a/doc/development/writing_documentation.md b/doc/development/writing_documentation.md
index eac9ec2a470..479258f743e 100644
--- a/doc/development/writing_documentation.md
+++ b/doc/development/writing_documentation.md
@@ -103,3 +103,24 @@ If that job fails, read the instructions in the job log for what to do next.
Contributors do not need to submit their changes to EE, GitLab Inc. employees
on the other hand need to make sure that their changes apply cleanly to both
CE and EE.
+
+## Previewing the changes live
+
+If you want to preview your changes live, you can use the manual `build-docs`
+job in your merge request.
+
+![Manual trigger a docs build](img/manual_build_docs.png)
+
+This job will:
+
+1. Create a new branch in the [gitlab-docs](https://gitlab.com/gitlab-com/gitlab-docs)
+ project named after the scheme: `<CE/EE-branch-slug>-built-from-ce-ee`
+1. Trigger a pipeline and build the docs site with your changes
+
+Look for the docs URL at the output of the `build-docs` job.
+
+>**Note:**
+Make sure that you always delete the branch of the merge request you were
+working on. If you don't, the remote docs branch won't be removed either,
+and the server where the Review Apps are hosted will eventually be out of
+disk space.
diff --git a/doc/install/database_mysql.md b/doc/install/database_mysql.md
index bc75dc1447e..5c128f54a76 100644
--- a/doc/install/database_mysql.md
+++ b/doc/install/database_mysql.md
@@ -75,7 +75,7 @@ log_bin_trust_function_creators=1
### MySQL utf8mb4 support
-After installation or upgrade, remember to [convert any new tables](#convert) to `utf8mb4`/`utf8mb4_general_ci`.
+After installation or upgrade, remember to [convert any new tables](#tables-and-data-conversion-to-utf8mb4) to `utf8mb4`/`utf8mb4_general_ci`.
---
@@ -230,7 +230,6 @@ We need to check, enable and probably convert your existing GitLab DB tables to
> Now, ensure that [innodb_file_format](https://dev.mysql.com/doc/refman/5.6/en/tablespace-enabling.html) and [innodb_large_prefix](http://dev.mysql.com/doc/refman/5.7/en/innodb-parameters.html#sysvar_innodb_large_prefix) are **persisted** in your `my.cnf` file.
#### Tables and data conversion to utf8mb4
-<a name="convert"></a>
Now that you have a persistent MySQL setup, you can safely upgrade tables after setup or upgrade time:
diff --git a/doc/install/kubernetes/gitlab_chart.md b/doc/install/kubernetes/gitlab_chart.md
index a339bc23809..177124c8291 100644
--- a/doc/install/kubernetes/gitlab_chart.md
+++ b/doc/install/kubernetes/gitlab_chart.md
@@ -1,9 +1,9 @@
# GitLab Helm Chart
-> **Note:**
-* > **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68).
-* Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **Note**:
+* This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68).
+* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
-The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. For most deployments we recommended the [gitlab-omnibus](gitlab_omnibus.md) chart,
+The `gitlab` Helm chart deploys just GitLab into your Kubernetes cluster, and offers extensive configuration options. This chart requires advanced knowledge of Kubernetes to successfully use. For most deployments we **strongly recommended** the [gitlab-omnibus](gitlab_omnibus.md) chart, which will replace this chart once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). Due to the difficulty in supporting upgrades to the `omnibus-gitlab` chart, migrating will require exporting data out of this instance and importing it into the new deployment.
This chart includes the following:
@@ -15,9 +15,11 @@ This chart includes the following:
- Optional PostgreSQL deployment using the [PostgreSQL Chart](https://github.com/kubernetes/charts/tree/master/stable/postgresql) (defaults to enabled)
- Optional Ingress (defaults to disabled)
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
+
## Prerequisites
-- _At least_ 3 GB of RAM available on your cluster, in chunks of 1 GB. 41GB of storage and 2 CPU are also required.
+- _At least_ 3 GB of RAM available on your cluster. 41GB of storage and 2 CPU are also required.
- Kubernetes 1.4+ with Beta APIs enabled
- [Persistent Volume](https://kubernetes.io/docs/concepts/storage/persistent-volumes/) provisioner support in the underlying infrastructure
- The ability to point a DNS entry or URL at your GitLab install
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index d7fd8613633..9d1280c3dc6 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -2,13 +2,15 @@
> **Note:**
* This Helm chart is in beta, while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being worked on.
* GitLab is working on a [cloud native set of Charts](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md) which will eventually replace these.
-* Officially supported cloud providers are Google Container Service and Azure Container Service.
+* These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
This work is based partially on: https://github.com/lwolf/kubernetes-gitlab/. GitLab would like to thank Sergey Nuzhdin for his work.
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
+
## Introduction
-This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned as well via [Let's Encrypt](https://letsencrypt.org/).
+This chart provides an easy way to get started with GitLab, provisioning an installation with nearly all functionality enabled. SSL is automatically provisioned via [Let's Encrypt](https://letsencrypt.org/).
The deployment includes:
@@ -19,7 +21,13 @@ The deployment includes:
- [NGINX Ingress](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress)
- Persistent Volume Claims for Data, Registry, Postgres, and Redis
-A video demonstration of GitLab utilizing this chart [is available](https://about.gitlab.com/handbook/sales/demo/).
+### Limitations
+
+* This chart is suited for small to medium size deployments, because [High Availability](https://docs.gitlab.com/ee/administration/high_availability/) and [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html) will not be supported.
+* It is in beta. Additional features to support production deployments, like backups, are [in development](https://gitlab.com/charts/charts.gitlab.io/issues/68). Once completed, this chart will be generally available.
+* A new generation of [cloud native charts](index.md#upcoming-cloud-native-helm-charts) is in development, and will eventually deprecate these. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment. We do not expect these to be production ready before the second half of 2018.
+
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
## Prerequisites
@@ -46,7 +54,7 @@ Finally, set the `baseIP` setting to this IP address when [deploying GitLab](#co
#### Load Balancer IP
-If you do not specify a `baseIP`, an ephemeral IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab:
+If you do not specify a `baseIP`, an IP will be assigned to the Load Balancer or Ingress. You can retrieve this IP by running the following command *after* deploying GitLab:
`kubectl get svc -w --namespace nginx-ingress nginx`
diff --git a/doc/install/kubernetes/gitlab_runner_chart.md b/doc/install/kubernetes/gitlab_runner_chart.md
index d31c763ed64..5e0d7493b61 100644
--- a/doc/install/kubernetes/gitlab_runner_chart.md
+++ b/doc/install/kubernetes/gitlab_runner_chart.md
@@ -1,6 +1,6 @@
# GitLab Runner Helm Chart
> **Note:**
-Officially supported cloud providers are Google Container Service and Azure Container Service.
+These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
The `gitlab-runner` Helm chart deploys a GitLab Runner instance into your
Kubernetes cluster.
@@ -11,6 +11,8 @@ This chart configures the Runner to:
- For each new job it receives from [GitLab CI](https://about.gitlab.com/features/gitlab-ci-cd/), it will provision a
new pod within the specified namespace to run it.
+For more information on available GitLab Helm Charts, please see our [overview](index.md#chart-overview).
+
## Prerequisites
- Your GitLab Server's API is reachable from the cluster
diff --git a/doc/install/kubernetes/index.md b/doc/install/kubernetes/index.md
index fb6c0c2d263..c799f88ad74 100644
--- a/doc/install/kubernetes/index.md
+++ b/doc/install/kubernetes/index.md
@@ -1,56 +1,59 @@
# Installing GitLab on Kubernetes
-> Officially supported cloud providers are Google Container Service and Azure Container Service.
+> **Note**: These charts have been tested on Google Container Engine and Azure Container Service. Other Kubernetes installations may work as well, if not please [open an issue](https://gitlab.com/charts/charts.gitlab.io/issues).
-The easiest method to deploy GitLab in [Kubernetes](https://kubernetes.io/) is
+The easiest method to deploy GitLab on [Kubernetes](https://kubernetes.io/) is
to take advantage of GitLab's Helm charts. [Helm] is a package
management tool for Kubernetes, allowing apps to be easily managed via their
Charts. A [Chart] is a detailed description of the application including how it
should be deployed, upgraded, and configured.
-GitLab provides [official Helm Charts](#official-gitlab-helm-charts-recommended) which are the recommended way to run GitLab within Kubernetes.
+## Chart Overview
-There are also two other sets of charts:
-* Our [upcoming cloud native Charts](#upcoming-cloud-native-helm-charts), which are in development but will eventually replace the current official charts.
-* [Community contributed charts](#community-contributed-helm-charts). These charts should be considered deprecated, in favor of the official charts.
+* **[GitLab-Omnibus](#gitlab-omnibus-chart-recommended)**: The best way to run GitLab on Kubernetes today. It is suited for small to medium deployments, and is in beta while support for backups and other features are added.
+* **[Upcoming Cloud Native Charts](#upcoming-cloud-native-helm-charts)**: The next generation of charts, currently in development. Will support large deployments, with horizontal scaling of individual GitLab components.
+* Other Charts
+ * [GitLab Runner Chart](#gitlab-runner-chart): For deploying just the GitLab Runner.
+ * [Advanced GitLab Installation](#advanced-gitlab-installation): Provides additional deployment options, but provides less functionality out-of-the-box. It's beta, no longer actively developed, and will be deprecated by [gitlab-omnibus](#gitlab-omnibus-chart-recommended) once it supports these options.
+ * [Community Contributed Charts](#community-contributed-charts): Community contributed charts, deprecated by the official GitLab charts.
-## Official GitLab Helm Charts
+## GitLab-Omnibus Chart (Recommended)
+> **Note**: This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being added.
-These charts utilize our [GitLab Omnibus Docker images](https://docs.gitlab.com/omnibus/docker/README.html). You can report any issues and feedback related to these charts at
-https://gitlab.com/charts/charts.gitlab.io/issues.
+This chart is the best available way to operate GitLab on Kubernetes. It deploys and configures nearly all features of GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [Mattermost](https://docs.gitlab.com/omnibus/gitlab-mattermost/), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and a [load balancer](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). It is based on our [GitLab Omnibus Docker Images](https://docs.gitlab.com/omnibus/docker/README.html).
-### Deploying GitLab on Kubernetes
-> **Note**: This chart will eventually be replaced by the [cloud native charts](#upcoming-cloud-native-helm-charts), which are presently in development.
+Once the [cloud native charts](#upcoming-cloud-native-helm-charts) are ready for production use, this chart will be deprecated. Due to the difficulty in supporting upgrades to the new architecture, migrating will require exporting data out of this instance and importing it into the new deployment.
-The best way to deploy GitLab on Kubernetes is to use the [gitlab-omnibus](gitlab_omnibus.md) chart.
+Learn more about the [gitlab-omnibus chart.](gitlab_omnibus.md)
-It includes everything needed to run GitLab, including: a [Runner](https://docs.gitlab.com/runner/), [Container Registry](https://docs.gitlab.com/ee/user/project/container_registry.html#gitlab-container-registry), [automatic SSL](https://github.com/kubernetes/charts/tree/master/stable/kube-lego), and an [Ingress](https://github.com/kubernetes/ingress/tree/master/controllers/nginx). This chart is in beta while [additional features](https://gitlab.com/charts/charts.gitlab.io/issues/68) are being completed.
+## Upcoming Cloud Native Charts
-### Deploying just the GitLab Runner
+GitLab is working towards building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into its [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended).
-To deploy just the [GitLab Runner](https://docs.gitlab.com/runner/), utilize the [gitlab-runner](gitlab_runner_chart.md) chart.
+By offering individual containers and charts, we will be able to provide a number of benefits:
+* Easier horizontal scaling of each service,
+* Smaller, more efficient images,
+* Potential for rolling updates and canaries within a service,
+* and plenty more.
-It offers a quick way to configure and deploy the Runner on Kubernetes, regardless of where your GitLab server may be running.
+This is a large project and will be worked on over the span of multiple releases. For the most up-to-date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420). We do not expect these to be production ready before the second half of 2018.
-### Advanced deployment of GitLab
-> **Note**: This chart will be replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68).
+## Other Charts
-If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the GitLab service along with optional Postgres and Redis. It offers extensive configuration, but requires deep knowledge of Kubernetes and Helm to use.
+### GitLab Runner Chart
-For most deployments we recommend using our [gitlab-omnibus](gitlab_omnibus.md) chart.
+If you already have a GitLab instance running, inside or outside of Kubernetes, and you'd like to leverage the Runner's [Kubernetes capabilities](https://gitlab.com/gitlab-org/gitlab-ci-multi-runner/blob/master/docs/executors/kubernetes.md), it can be deployed with the GitLab Runner chart.
-## Upcoming Cloud Native Helm Charts
+Learn more about [gitlab-runner chart.](gitlab_runner_chart.md)
-GitLab is working towards a building a [cloud native deployment method](https://gitlab.com/charts/helm.gitlab.io/blob/master/README.md). A key part of this effort is to isolate each service into it's [own Docker container and Helm chart](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420), rather than utilizing the all-in-one container image of the [current charts](#official-gitlab-helm-charts-recommended).
+### Advanced GitLab Installation
-By offering individual containers and charts, we will be able to provide a number of benefits:
-* Easier horizontal scaling of each service
-* Smaller more efficient images
-* Potential for rolling updates and canaries within a service
-* and plenty more.
+If advanced configuration of GitLab is required, the beta [gitlab](gitlab_chart.md) chart can be used which deploys the core GitLab service along with optional Postgres and Redis. It offers extensive configuration, but offers limited functionality out-of-the-box; it's lacking Pages support, the container registry, and Mattermost. It requires deep knowledge of Kubernetes and Helm to use.
+
+This chart will be deprecated and replaced by the [gitlab-omnibus](gitlab_omnibus.md) chart, once it supports [additional configuration options](https://gitlab.com/charts/charts.gitlab.io/issues/68). It's beta quality, and since it is not actively under development, it will never be GA.
-This is a large project and will be worked on over the span of multiple releases. For the most up to date status and release information, please see our [tracking issue](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/2420).
+Learn more about the [gitlab chart.](gitlab_chart.md)
-## Community Contributed Helm Charts
+### Community Contributed Charts
The community has also [contributed GitLab charts](https://github.com/kubernetes/charts/tree/master/stable/gitlab-ce) to the [Helm Stable Repository](https://github.com/kubernetes/charts#repository-structure). These charts should be considered [deprecated](https://github.com/kubernetes/charts/issues/1138) in favor of the [official Charts](#official-gitlab-helm-charts-recommended).
diff --git a/doc/topics/autodevops/img/auto_devops_settings.png b/doc/topics/autodevops/img/auto_devops_settings.png
new file mode 100644
index 00000000000..57bd7650a30
--- /dev/null
+++ b/doc/topics/autodevops/img/auto_devops_settings.png
Binary files differ
diff --git a/doc/topics/autodevops/img/auto_monitoring.png b/doc/topics/autodevops/img/auto_monitoring.png
new file mode 100644
index 00000000000..5661b50841b
--- /dev/null
+++ b/doc/topics/autodevops/img/auto_monitoring.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_connect_cluster.png b/doc/topics/autodevops/img/guide_connect_cluster.png
new file mode 100644
index 00000000000..b856b81a1d0
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_connect_cluster.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_integration.png b/doc/topics/autodevops/img/guide_integration.png
new file mode 100644
index 00000000000..723b2619ea2
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_integration.png
Binary files differ
diff --git a/doc/topics/autodevops/img/guide_secret.png b/doc/topics/autodevops/img/guide_secret.png
new file mode 100644
index 00000000000..01f5aa49908
--- /dev/null
+++ b/doc/topics/autodevops/img/guide_secret.png
Binary files differ
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
new file mode 100644
index 00000000000..babf44d2665
--- /dev/null
+++ b/doc/topics/autodevops/index.md
@@ -0,0 +1,342 @@
+# Auto DevOps
+
+> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and
+**not recommended for production use**. Access to the Container Registry is only
+available while the pipeline is running. Restarting a pod, scaling a service, or
+other actions which require on-going access **will fail** even for public
+projects. On-going secure access is planned for a subsequent release.
+
+Auto DevOps brings best practices to your project in an easy and default way. A
+typical web project starts with Continuous Integration (CI), then adds automated
+deployment to production, and maybe some time in the future adds some kind of
+monitoring. With Auto DevOps, every project has a complete workflow, with
+no configuration, including:
+
+- [Auto Build](#auto-build)
+- [Auto Test](#auto-test)
+- [Auto Code Quality](#auto-code-quality)
+- [Auto Review Apps](#auto-review-apps)
+- [Auto Deploy](#auto-deploy)
+- [Auto Monitoring](#-auto-monitoring)
+
+## Overview
+
+You will need [Kubernetes](https://kubernetes.io/) and
+[Prometheus](https://prometheus.io/) to make full use of Auto DevOps, but
+even projects using only [GitLab Runners](https://docs.gitlab.com/runner/) will
+be able to make use of Auto Build, Auto Test, and Auto Code Quality.
+
+Auto DevOps makes use of an open source tool called
+[Herokuish](https://github.com/gliderlabs/herokuish) which uses [Heroku
+buildpacks](https://devcenter.heroku.com/articles/buildpacks) to automatically
+detect, build, and test applications. Auto DevOps supports all of the languages
+and frameworks that are [supported by
+Herokuish](https://github.com/gliderlabs/herokuish#buildpacks) such as Ruby,
+Rails, Node, PHP, Python, and Java, and [custom buildpacks can be
+specified](#using-custom-buildpacks). *GitLab is in no way affiliated with Heroku
+or Glider Labs.*
+
+Projects can [customize](#customizing) the process by specifying [custom
+buildpacks](#custom-buildpack), [custom `Dockerfile`s](#custom-dockerfile),
+[custom Helm charts](#custom-helm-chart), or even copying the complete CI/CD
+configuration into your project to enable staging and canary deployments, and
+more.
+
+## Quick start
+
+If you are using GitLab.com, see our [quick start guide](quick_start_guide.md)
+for using Auto DevOps with GitLab.com and an external Kubernetes cluster on
+Google Cloud.
+
+For self-hosted installations, the easiest way to make use of Auto DevOps is to
+install GitLab inside a Kubernetes cluster using the [GitLab-Omnibus Helm
+Chart](../../install/kubernetes/gitlab_omnibus.md) which automatically installs
+and configures everything you need.
+
+## Prerequisites
+
+You will need one or more GitLab Runners, a Kubernetes cluster, and Prometheus
+installed in the cluster to make full use of Auto DevOps. If you do not have
+Kubernetes or Prometheus installed then Auto Review Apps, Auto Deploy, and Auto
+Monitoring will be silently skipped.
+
+If you are using GitLab outside of Kubernetes, for example with GitLab.com, then
+you should take these prerequisites into account:
+
+1. **Base domain** - You will need a base domain configured with wildcard DNS to
+ be used by all of your Auto DevOps applications.
+
+1. **GitLab Runner** - Your Runner needs to be configured to be able to run Docker.
+ Generally this means using the
+ [Docker](https://docs.gitlab.com/runner/executors/docker.html) or [Kubernetes
+ executor](https://docs.gitlab.com/runner/executors/kubernetes.html), with
+ [privileged mode enabled](https://docs.gitlab.com/runner/executors/docker.html#use-docker-in-docker-with-privileged-mode).
+ The Runners do not need to be installed in the Kubernetes cluster, but the
+ Kubernetes executor is easy to use and is automatically autoscaling.
+ Docker-based Runners can be configured to autoscale as well, using [Docker
+ Machine](https://docs.gitlab.com/runner/install/autoscaling.html). Runners
+ should be registered as [shared Runners](../../ci/runners/README.md#registering-a-shared-runner)
+ for the entire GitLab instance, or [specific Runners](../../ci/runners/README.md#registering-a-specific-runner)
+ that are assigned to specific projects.
+
+1. **Kubernetes** - To enable deploys, you will need Kubernetes 1.5+, with NGINX
+ ingress and wildcard SSL termination, for example using the
+ [`nginx-ingress`](https://github.com/kubernetes/charts/tree/master/stable/nginx-ingress)
+ and [`kube-lego`](https://github.com/kubernetes/charts/tree/master/stable/kube-lego)
+ Helm charts respectively. The [Kubernetes service][kubernetes-service]
+ integration will need to be enabled for the project, or enabled as a
+ [default service template](../../user/project/integrations/services_templates.md)
+ for the entire GitLab installation.
+
+1. **Prometheus** - To enable Auto Monitoring, you will need Prometheus installed
+ somewhere (inside or outside your cluster) and configured to scrape your
+ Kubernetes cluster. To get response metrics (in addition to system metrics),
+ you need to [configure Prometheus to monitor NGINX](../../user/project/integrations/prometheus_library/nginx_ingress.md#configuring-prometheus-to-monitor-for-nginx-ingress-metrics).
+ The [Prometheus service](../../user/project/integrations/prometheus.md)
+ integration needs to be enabled for the project, or enabled as a
+ [default service template](../../user/project/integrations/services_templates.md)
+ for the entire GitLab installation.
+
+## Enabling Auto DevOps
+
+In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps
+section. Select "Enable Auto DevOps", add in your base domain, and save.
+
+![auto devops settings](img/auto_devops_settings.png)
+
+## Stages of Auto DevOps
+
+The following sections describe the stages of Auto DevOps.
+
+### Auto Build
+
+Auto Build creates a build of the application in one of two ways:
+
+- If there is a `Dockerfile`, it will use `docker build` to create a Docker image.
+- Otherwise, it will use [Herokuish](https://github.com/gliderlabs/herokuish)
+ and [Heroku buildpacks](https://devcenter.heroku.com/articles/buildpacks)
+ to automatically detect and build the application into a Docker image.
+
+Either way, the resulting Docker image is automatically pushed to the
+[Container Registry][container-registry], tagged with the commit SHA.
+
+### Auto Test
+
+Auto Test automatically tests your application using
+[Herokuish](https://github.com/gliderlabs/herokuish) and [Heroku
+buildpacks](https://devcenter.heroku.com/articles/buildpacks). Auto Test will
+analyze your project to detect the language and framework, and run appropriate
+tests. Several languages and frameworks are detected automatically, but if your
+language is not detected, you may succeed with a [custom
+buildpack](#custom-buildpack).
+
+Auto Test uses tests you already have in your application. If there are no
+tests, it's up to you to add them.
+
+### Auto Code Quality
+
+Auto Code Quality uses the open source
+[`codeclimate` image](https://hub.docker.com/r/codeclimate/codeclimate/) to run
+static analysis and other code checks on the current code, creating a report
+that is uploaded as an artifact. In GitLab EE, differences between the source
+and target branches are shown in the merge request widget. *GitLab is in no way
+affiliated with Code Climate.*
+
+### Auto Review Apps
+
+Auto Review Apps create a [Review App][review-app] for each branch. Review Apps
+are temporary application environments based on the branch's code so developers,
+designers, QA, product managers, and other reviewers can actually see and
+interact with code changes as part of the review process.
+
+The review app will have a unique URL based on the project name, the branch
+name, and a unique number, combined with the Auto DevOps base domain. For
+example, `user-project-branch-1234.example.com`. A link to the Review App shows
+up in the merge request widget for easy discovery. When the branch is deleted,
+for example after the merge request is merged, the Review App will automatically
+be deleted.
+
+This is an optional step, since many projects do not have a Kubernetes cluster
+available. If the Kubernetes service is not configured, or if the variable
+`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto
+DevOps setting), the job will silently be skipped.
+
+### Auto Deploy
+
+After a branch or merge request is merged into `master`, Auto Deploy deploys the
+application to a `production` environment in the Kubernetes cluster, with a
+namespace based on the project name and unique project ID. e.g. `project-4321`.
+This is an optional step, since many projects do not have a Kubernetes cluster
+available. If the Kubernetes service is not configured, or if the variable
+`AUTO_DEVOPS_DOMAIN` is not available (usually set automatically by the Auto
+DevOps setting), the job will silently be skipped.
+
+Auto Deploy doesn't include deployments to staging or canary by default, but the
+Auto DevOps template contains job definitions for these tasks if you want to
+enable them.
+
+### Auto Monitoring
+
+Once your application is deployed, Auto Monitoring makes it possible to monitor
+your application's server and response metrics right out of the box. Auto
+Monitoring uses [Prometheus](../../user/project/integrations/prometheus.md) to
+get system metrics such as CPU and memory usage directly from
+[Kubernetes](../../user/project/integrations/prometheus_library/kubernetes.md),
+and response metrics such as HTTP error rates, latency, and throughput from the
+[NGINX
+server](../../user/project/integrations/prometheus_library/nginx_ingress.md).
+
+* Response Metrics: latency, throughput, error rate
+* System Metrics: CPU utilization, memory utilization
+
+To view the metrics, open the [Monitoring dashboard for a deployed environment](../../ci/environments.md#monitoring-environments).
+
+![Auto Metrics](img/auto_monitoring.png)
+
+### Configuring Auto Monitoring
+
+If GitLab has been deployed using the
+[omnibus-gitlab](../../install/kubernetes/gitlab_omnibus.md) Helm chart, no
+configuration is required.
+
+If you have installed GitLab using a different method:
+
+1. [Deploy Prometheus](../../user/project/integrations/prometheus.md#configuring-your-own-prometheus-server-within-kubernetes) into your Kubernetes cluster
+1. If you would like response metrics, ensure you are running at least version 0.9.0 of NGINX Ingress and [enable Prometheus metrics](https://github.com/kubernetes/ingress/blob/master/examples/customization/custom-vts-metrics/nginx/nginx-vts-metrics-conf.yaml).
+1. Finally, [annotate](https://kubernetes.io/docs/concepts/overview/working-with-objects/annotations/) the NGINX Ingress deployment to be scraped by Prometheus using `prometheus.io/scrape: "true"` and `prometheus.io/port: "10254"`.
+
+## Customizing
+
+### PostgreSQL Database Support
+
+In order to support applications that require a database,
+[PostgreSQL][postgresql] is provisioned by default. Credentials to access the
+database are preconfigured, but can be customized by setting the associated
+[variables](#postgresql-variables). These credentials can be used for defining a
+`DATABASE_URL` of the format:
+`postgres://user:password@postgres-host:postgres-port/postgres-database`.
+
+PostgreSQL provisioning can be disabled by creating a project variable
+`POSTGRES_ENABLED` set to `false`.
+
+#### PostgreSQL Variables
+
+Any variables set at the project or group level will override variables set in
+the CI/CD configuration.
+
+1. `POSTGRES_ENABLED: "false"`: disable automatic deployment of PostgreSQL
+1. `POSTGRES_USER: "my-user"`: use custom username for PostgreSQL
+1. `POSTGRES_PASSWORD: "password"`: use custom password for PostgreSQL
+1. `POSTGRES_DB: "my-database"`: use custom database name for PostgreSQL
+
+### Custom buildpack
+
+If the automatic buildpack detection fails for your project, or if you want to
+use a custom buildpack, you can override the buildpack using a project variable
+or a `.buildpack` file in your project:
+
+- **Project variable** - Create a project variable `BUILDPACK_URL` with the URL
+ of the buildpack to use.
+
+- **`.buildpack` file** - Add a file in your project's repo called `.buildpack`
+ and add the URL of the buildpack to use on a line in the file. If you want to
+ use multiple buildpacks, you can enter them in, one on each line
+
+ >**Note:** Using multiple buildpacks may break Auto Test.
+
+### Custom `Dockerfile`
+
+If your project has a `Dockerfile` in the root of the project repo, Auto DevOps
+will build a Docker image based on the Dockerfile rather than using buildpacks.
+This can be much faster and result in smaller images, especially if your
+Dockerfile is based on [Alpine](https://hub.docker.com/_/alpine/).
+
+### Custom Helm Chart
+
+Auto DevOps uses Helm to deploy your application to Kubernetes. You can override
+the Helm chart used by bundling up a chart into your project repo or by
+specifying a project variable.
+
+**Bundled chart** - If your project has a `chart` directory with a `Chart.yaml`
+file in it, Auto DevOps will detect the chart and use it instead of the default
+chart. This can be a great way to control exactly how your application is
+deployed.
+
+**Project variable** - Create a project variable `AUTO_DEVOPS_CHART` with the
+URL of a custom chart to use.
+
+### Enable staging, canaries, and more with custom `.gitlab-ci.yml`
+
+If you want to modify the CI/CD pipeline used by Auto DevOps, you can copy the
+Auto DevOps template into your project's repo and edit as you see fit.
+
+From your project home page, click on the `Set up CI` button, or click on the `+`
+button and `New file` and pick `.gitlab-ci.yml` as the template type, or view an
+existing `.gitlab-ci.yml` file. Then select "Auto DevOps" from the template
+dropdown. You will then be able to edit or add any jobs needed.
+
+For example, if you want deploys to go to a staging environment instead of
+directly to a production environment, you can enable the `staging` job by
+renaming `.staging` to `staging`. Then make sure to uncomment the `when` key of
+the `production` job to turn it into a manual action instead of deploying
+automatically.
+
+## Currently supported languages
+
+>**Note:**
+Not all buildpacks support Auto Test yet, as it's a relatively new
+enhancement. All of Heroku's [officially supported
+languages](https://devcenter.heroku.com/articles/heroku-ci#currently-supported-languages)
+support it, and some third-party buildpacks as well e.g., Go, Node, Java, PHP,
+Python, Ruby, Gradle, Scala, and Elixir all support Auto Test, but notably the
+multi-buildpack does not.
+
+As of GitLab 10.0, the supported buildpacks are:
+
+```
+* heroku-buildpack-multi v1.0.0
+* heroku-buildpack-ruby v168
+* heroku-buildpack-nodejs v99
+* heroku-buildpack-clojure v77
+* heroku-buildpack-python v99
+* heroku-buildpack-java v53
+* heroku-buildpack-gradle v23
+* heroku-buildpack-scala v78
+* heroku-buildpack-play v26
+* heroku-buildpack-php v122
+* heroku-buildpack-go v72
+* heroku-buildpack-erlang fa17af9
+* buildpack-nginx v8
+```
+
+## Private Project Support - Experimental
+
+When a project has been marked as private, GitLab's [Container
+Registry][container-registry] requires authentication when downloading
+containers. Auto DevOps will automatically provide the required authentication
+information to Kubernetes, allowing temporary access to the registry.
+Authentication credentials will be valid while the pipeline is running, allowing
+for a successful initial deployment.
+
+After the pipeline completes, Kubernetes will no longer be able to access the
+container registry. **Restarting a pod, scaling a service, or other actions which
+require on-going access to the registry will fail**. On-going secure access is
+planned for a subsequent release.
+
+## Troubleshooting
+
+- Auto Build and Auto Test may fail in detecting your language/framework. There
+ may be no buildpack for your application, or your application may be missing the
+ key files the buildpack is looking for. For example, for ruby apps, you must
+ have a `Gemfile` to be properly detected, even though it is possible to write a
+ Ruby app without a `Gemfile`. Try specifying a [custom
+ buildpack](#custom-buildpack).
+- Auto Test may fail because of a mismatch between testing frameworks. In this
+ case, you may need to customize your `.gitlab-ci.yml` with your test commands.
+
+[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
+[kubernetes-service]: ../../user/project/integrations/kubernetes.md
+[docker-in-docker]: ../../docker/using_docker_build.md#use-docker-in-docker-executor
+[review-app]: ../../ci/review_apps/index.md
+[container-registry]: ../../user/project/container_registry.md
+[postgresql]: https://www.postgresql.org/
diff --git a/doc/topics/autodevops/quick_start_guide.md b/doc/topics/autodevops/quick_start_guide.md
new file mode 100644
index 00000000000..f23c9d794b4
--- /dev/null
+++ b/doc/topics/autodevops/quick_start_guide.md
@@ -0,0 +1,95 @@
+# Auto DevOps: quick start guide
+
+> [Introduced][ce-37115] in GitLab 10.0. Auto DevOps is currently in Beta and
+**not recommended for production use**.
+
+This is a step-by-step guide to deploying a project hosted on GitLab.com to
+Google Cloud, using Auto DevOps.
+
+We made a minimal [Ruby
+application](https://gitlab.com/gitlab-examples/minimal-ruby-app) to use as an
+example for this guide. It contains two files:
+
+* `server.rb` - our application. It will start an HTTP server on port 5000 and
+ render "Hello, world!"
+* `Dockerfile` - to build our app into a container image. It will use a ruby
+ base image and run `server.rb`
+
+## Fork sample project on GitLab.com
+
+Let’s start by forking our sample application. Go to [the project
+page](https://gitlab.com/gitlab-examples/minimal-ruby-app) and press the `Fork`
+button. Soon you should have a project under your namespace with the necessary
+files.
+
+## Setup your own cluster on Google Container Engine
+
+If you do not already have a Google Cloud account, create one at https://console.cloud.google.com.
+
+Visit the [`Container Engine`](https://console.cloud.google.com/kubernetes/list) tab and create a new cluster. You can change the name and leave the rest of the default settings. Once you have your cluster running, you need to connect to the cluster by following the Google interface.
+
+## Connect to Kubernetes cluster
+
+You need to have the Google Cloud SDK installed. e.g.
+On OSX, install [homebrew](https://brew.sh):
+
+1. Install Brew Caskroom: `brew install caskroom/cask/brew-cask`
+2. Install Google Cloud SDK: `brew cask install google-cloud-sdk`
+3. Add `kubectl`: `gcloud components install kubectl`
+4. Log in: `gcloud auth login`
+
+Now go back to the Google interface, find your cluster, and follow the instructions under `Connect to the cluster` and open the Kubernetes Dashboard. It will look something like `gcloud container clusters get-credentials ruby-autodeploy \ --zone europe-west2-c --project api-project-XXXXXXX` and then `kubectl proxy`.
+
+![connect to cluster](img/guide_connect_cluster.png)
+
+## Copy credentials to GitLab.com project
+
+Once you have the Kubernetes Dashboard interface running, you should visit `Secrets` under the `Config` section. There you should find the settings we need for GitLab integration: ca.crt and token.
+
+![connect to cluster](img/guide_secret.png)
+
+You need to copy-paste the ca.crt and token into your project on GitLab.com in the Kubernetes integration page under project **Settings > Integrations > Project services > Kubernetes**. Don't actually copy the namespace though. Each project should have a unique namespace, and by leaving it blank, GitLab will create one for you.
+
+![connect to cluster](img/guide_integration.png)
+
+For API URL, you should use the `Endpoint` IP from your cluster page on Google Cloud Platform.
+
+## Expose application to the world
+
+In order to be able to visit your application, you need to install an NGINX ingress controller and point your domain name to its external IP address.
+
+### Set up Ingress controller
+
+You’ll need to make sure you have an ingress controller. If you don’t have one, do:
+
+```sh
+brew install kubernetes-helm
+helm init
+helm install --name ruby-app stable/nginx-ingress
+```
+
+This should create several services including `ruby-app-nginx-ingress-controller`. You can list your services by running `kubectl get svc` to confirm that.
+
+### Point DNS at Cluster IP
+
+Find out the external IP address of the `ruby-app-nginx-ingress-controller` by running:
+
+```sh
+kubectl get svc ruby-app-nginx-ingress-controller -o jsonpath='{.status.loadBalancer.ingress[0].ip}'
+```
+
+Use this IP address to configure your DNS. This part heavily depends on your preferences and domain provider. But in case you are not sure, just create an A record with a wildcard host like `*.<your-domain>`.
+
+Use `nslookup minimal-ruby-app-staging.<yourdomain>` to confirm that domain is assigned to the cluster IP.
+
+## Set up Auto DevOps
+
+In your GitLab.com project, go to **Settings > CI/CD** and find the Auto DevOps section. Select "Enable Auto DevOps", add in your base domain, and save.
+
+![auto devops settings](img/auto_devops_settings.png)
+
+Then trigger your first pipeline run. This will create a new pipeline with several jobs: `build`, `test`, `codequality`, and `production`. The `build` job will create a docker image with your new change and push it to the GitLab Container Registry. The `test` job will test your change. The `codequality` job will run static analysis on your change. The `production` job will deploy your change to a production application. Once the deploy job succeeds you should be able to see your application by visiting the Kubernetes dashboard. Select the namespace of your project, which will look like `minimal-ruby-app-23`, but with a unique ID for your project, and your app will be listed as "production" under the Deployment tab.
+
+Once its ready - just visit http://minimal-ruby-app.example.com to see “Hello, world!”
+
+[ce-37115]: https://gitlab.com/gitlab-org/gitlab-ce/issues/37115
diff --git a/doc/topics/index.md b/doc/topics/index.md
index ad388dff822..b51f24b02e4 100644
--- a/doc/topics/index.md
+++ b/doc/topics/index.md
@@ -7,6 +7,7 @@ you through better understanding GitLab's concepts
through our regular docs, and, when available, through articles (guides,
tutorials, technical overviews, blog posts) and videos.
+- [Auto DevOps](autodevops/index.md)
- [Authentication](authentication/index.md)
- [Continuous Integration (GitLab CI)](../ci/README.md)
- [Git](git/index.md)
diff --git a/doc/user/group/img/share_with_group_lock.png b/doc/user/group/img/share_with_group_lock.png
index 8df41bf9465..c0f25389eaf 100644
--- a/doc/user/group/img/share_with_group_lock.png
+++ b/doc/user/group/img/share_with_group_lock.png
Binary files differ
diff --git a/doc/user/group/index.md b/doc/user/group/index.md
index fbc05261a32..db0242f1324 100644
--- a/doc/user/group/index.md
+++ b/doc/user/group/index.md
@@ -168,8 +168,7 @@ GitLab administrators can use the admin interface to move any project to any nam
You can [share your projects with a group](../project/members/share_project_with_groups.md)
and give your group members access to the project all at once.
-Alternatively, with [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
-you can [lock the sharing with group feature](#share-with-group-lock-ees-eep).
+Alternatively, you can [lock the sharing with group feature](#share-with-group-lock).
## Manage group memberships via LDAP
@@ -191,10 +190,28 @@ access further configurations for your group.
#### Enforce 2FA to group members
-Add a secury layer to your group by
+Add a security layer to your group by
[enforcing two-factor authentication (2FA)](../../security/two_factor_authentication.md#enforcing-2fa-for-all-users-in-a-group)
to all group members.
+#### Share with group lock
+
+Prevent projects in a group from [sharing
+a project with another group](../project/members/share_project_with_groups.md).
+This allows for tighter control over project access.
+
+For example, consider you have two distinct teams (Group A and Group B)
+working together in a project.
+To inherit the group membership, you share the project between the
+two groups A and B. **Share with group lock** prevents any project within
+the group from being shared with another group. By doing so, you
+guarantee only the right group members have access to that projects.
+
+To enable this feature, navigate to the group settings page. Select
+**Share with group lock** and **Save the group**.
+
+![Checkbox for share with group lock](img/share_with_group_lock.png)
+
#### Member Lock (EES/EEP)
Available in [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/),
@@ -203,15 +220,6 @@ level of members in group.
Learn more about [Member Lock](https://docs.gitlab.com/ee/user/group/index.html#member-lock-ees-eep).
-#### Share with group lock (EES/EEP)
-
-In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
-it is possible to prevent projects in a group from [sharing
-a project with another group](../project/members/share_project_with_groups.md).
-This allows for tighter control over project access.
-
-Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
-
### Advanced settings
- **Projects**: view all projects within that group, add members to each project,
diff --git a/doc/user/project/issues/confidential_issues.md b/doc/user/project/issues/confidential_issues.md
index 1760b182114..0bf1f396f9d 100644
--- a/doc/user/project/issues/confidential_issues.md
+++ b/doc/user/project/issues/confidential_issues.md
@@ -9,7 +9,7 @@ keep security vulnerabilities private or prevent surprises from leaking out.
## Making an issue confidential
-You can make an issue confidential either by creating a new issue or editing
+You can make an issue confidential during issue creation or by editing
an existing one.
When you create a new issue, a checkbox right below the text area is available
@@ -19,11 +19,19 @@ confidential checkbox and hit **Save changes**.
![Creating a new confidential issue](img/confidential_issues_create.png)
-## Making an issue non-confidential
+## Modifying issue confidentiality
-To make an issue non-confidential, all you have to do is edit it and unmark
-the confidential checkbox. Once you save the issue, it will gain the default
-visibility level you have chosen for your project.
+There are two ways to change an issue's confidentiality.
+
+The first way is to edit the issue and mark/unmark the confidential checkbox.
+Once you save the issue, it will change the confidentiality of the issue.
+
+The second way is to locate the Confidentiality section in the sidebar and click
+**Edit**. A popup should appear and give you the option to turn on or turn off confidentiality.
+
+| Turn off confidentiality | Turn on confidentiality |
+| :-----------: | :----------: |
+| ![Turn off confidentiality](img/turn_off_confidentiality.png) | ![Turn on confidentiality](img/turn_on_confidentiality.png) |
Every change from regular to confidential and vice versa, is indicated by a
system note in the issue's comments.
@@ -49,6 +57,12 @@ issue you are commenting on is confidential.
![Confidential issue page](img/confidential_issues_issue_page.png)
+There is also an indicator on the sidebar denoting confidentiality.
+
+| Confidential issue | Not confidential issue |
+| :-----------: | :----------: |
+| ![Sidebar confidential issue](img/sidebar_confidential_issue.png) | ![Sidebar not confidential issue](img/sidebar_not_confidential_issue.png) |
+
## Permissions and access to confidential issues
There are two kinds of level access for confidential issues. The general rule
diff --git a/doc/user/project/issues/img/confidential_issues_index_page.png b/doc/user/project/issues/img/confidential_issues_index_page.png
index e4b492a2769..f3efe0ce04e 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_index_page.png
+++ b/doc/user/project/issues/img/confidential_issues_index_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/confidential_issues_issue_page.png b/doc/user/project/issues/img/confidential_issues_issue_page.png
index f04ec8ff32b..0f5c774d258 100755..100644
--- a/doc/user/project/issues/img/confidential_issues_issue_page.png
+++ b/doc/user/project/issues/img/confidential_issues_issue_page.png
Binary files differ
diff --git a/doc/user/project/issues/img/sidebar_confidential_issue.png b/doc/user/project/issues/img/sidebar_confidential_issue.png
new file mode 100755
index 00000000000..d99a1ca756e
--- /dev/null
+++ b/doc/user/project/issues/img/sidebar_confidential_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/sidebar_not_confidential_issue.png b/doc/user/project/issues/img/sidebar_not_confidential_issue.png
new file mode 100755
index 00000000000..2e6cbbc5b3a
--- /dev/null
+++ b/doc/user/project/issues/img/sidebar_not_confidential_issue.png
Binary files differ
diff --git a/doc/user/project/issues/img/turn_off_confidentiality.png b/doc/user/project/issues/img/turn_off_confidentiality.png
new file mode 100644
index 00000000000..248ae6522d6
--- /dev/null
+++ b/doc/user/project/issues/img/turn_off_confidentiality.png
Binary files differ
diff --git a/doc/user/project/issues/img/turn_on_confidentiality.png b/doc/user/project/issues/img/turn_on_confidentiality.png
new file mode 100644
index 00000000000..fac4c833699
--- /dev/null
+++ b/doc/user/project/issues/img/turn_on_confidentiality.png
Binary files differ
diff --git a/doc/user/project/members/share_project_with_groups.md b/doc/user/project/members/share_project_with_groups.md
index 25e5b897825..f5c748a03b3 100644
--- a/doc/user/project/members/share_project_with_groups.md
+++ b/doc/user/project/members/share_project_with_groups.md
@@ -22,7 +22,7 @@ To share 'Project Acme' with the 'Engineering' group, go to the project settings
Then select the 'Share with group' tab by clicking it.
-Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it.
+Now you can add the 'Engineering' group with the maximum access level of your choice. Click 'Share' to share it.
![share project with groups tab](img/share_project_with_groups_tab.png)
@@ -34,11 +34,10 @@ After sharing 'Project Acme' with 'Engineering', the project will be listed on t
In the example above, the maximum access level of 'Developer' for members from 'Engineering' means that users with higher access levels in 'Engineering' ('Master' or 'Owner') will only have 'Developer' access to 'Project Acme'.
-## Share project with group lock (EES/EEP)
+## Share project with group lock
-In [GitLab Enterprise Edition Starter](https://about.gitlab.com/gitlab-ee/)
-it is possible to prevent projects in a group from [sharing
+It is possible to prevent projects in a group from [sharing
a project with another group](../members/share_project_with_groups.md).
This allows for tighter control over project access.
-Learn more about [Share with group lock](https://docs.gitlab.com/ee/user/group/index.html#share-with-group-lock-ees-eep).
+Learn more about [Share with group lock](../../group/index.html#share-with-group-lock).
diff --git a/features/steps/project/fork.rb b/features/steps/project/fork.rb
index 513ccce2f8f..f88738b4c61 100644
--- a/features/steps/project/fork.rb
+++ b/features/steps/project/fork.rb
@@ -52,7 +52,7 @@ class Spinach::Features::ProjectFork < Spinach::FeatureSteps
end
step 'I visit the forks page of the "Shop" project' do
- @project = Project.where(name: 'Shop').last
+ @project = Project.where(name: 'Shop').first
visit project_forks_path(@project)
end
diff --git a/features/steps/project/project.rb b/features/steps/project/project.rb
index 0a89c1baf20..3a762be8f1f 100644
--- a/features/steps/project/project.rb
+++ b/features/steps/project/project.rb
@@ -6,7 +6,6 @@ class Spinach::Features::Project < Spinach::FeatureSteps
step 'change project settings' do
fill_in 'project_name_edit', with: 'NewName'
- select 'Disabled', from: 'project_project_feature_attributes_issues_access_level'
end
step 'I save project' do
diff --git a/features/steps/shared/project.rb b/features/steps/shared/project.rb
index 605c9a3ab71..96cc0745e97 100644
--- a/features/steps/shared/project.rb
+++ b/features/steps/shared/project.rb
@@ -89,7 +89,7 @@ module SharedProject
step 'I should see project settings' do
expect(current_path).to eq edit_project_path(@project)
expect(page).to have_content("Project name")
- expect(page).to have_content("Sharing and permissions")
+ expect(page).to have_content("Permissions")
end
def current_project
diff --git a/lib/api/api.rb b/lib/api/api.rb
index d9a62bffb6d..ee4e1688e12 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -2,6 +2,17 @@ module API
class API < Grape::API
include APIGuard
+ LOG_FILENAME = Rails.root.join("log", "api_json.log")
+
+ use GrapeLogging::Middleware::RequestLogger,
+ logger: Logger.new(LOG_FILENAME),
+ formatter: Gitlab::GrapeLogging::Formatters::LogrageWithTimestamp.new,
+ include: [
+ GrapeLogging::Loggers::Response.new,
+ GrapeLogging::Loggers::FilterParameters.new,
+ GrapeLogging::Loggers::ClientEnv.new
+ ]
+
allow_access_with_scope :api
prefix :api
diff --git a/lib/api/broadcast_messages.rb b/lib/api/broadcast_messages.rb
index 0b45621ce7b..d7138b2f2fe 100644
--- a/lib/api/broadcast_messages.rb
+++ b/lib/api/broadcast_messages.rb
@@ -20,7 +20,7 @@ module API
use :pagination
end
get do
- messages = BroadcastMessage.all
+ messages = BroadcastMessage.all.order_id_desc
present paginate(messages), with: Entities::BroadcastMessage
end
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 216408064d1..52c49e5caa9 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -45,7 +45,7 @@ module API
expose :confirmed_at
expose :last_activity_on
expose :email
- expose :color_scheme_id, :projects_limit, :current_sign_in_at
+ expose :theme_id, :color_scheme_id, :projects_limit, :current_sign_in_at
expose :identities, using: Entities::Identity
expose :can_create_group?, as: :can_create_group
expose :can_create_project?, as: :can_create_project
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index 31a918eda60..e817dcbbc4b 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -74,7 +74,12 @@ module API
use :optional_params
end
post do
- authorize! :create_group
+ parent_group = find_group!(params[:parent_id]) if params[:parent_id].present?
+ if parent_group
+ authorize! :create_subgroup, parent_group
+ else
+ authorize! :create_group
+ end
group = ::Groups::CreateService.new(current_user, declared_params(include_missing: false)).execute
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index 8b03df65ae4..00dbc2aee7a 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -93,7 +93,7 @@ module API
end
def find_group(id)
- if id =~ /^\d+$/
+ if id.to_s =~ /^\d+$/
Group.find_by(id: id)
else
Group.find_by_full_path(id)
diff --git a/lib/api/merge_request_diffs.rb b/lib/api/merge_request_diffs.rb
index c3affcc6c6b..95ef8f42954 100644
--- a/lib/api/merge_request_diffs.rb
+++ b/lib/api/merge_request_diffs.rb
@@ -21,7 +21,7 @@ module API
get ":id/merge_requests/:merge_request_iid/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_iid])
- present paginate(merge_request.merge_request_diffs), with: Entities::MergeRequestDiff
+ present paginate(merge_request.merge_request_diffs.order_id_desc), with: Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
diff --git a/lib/api/milestone_responses.rb b/lib/api/milestone_responses.rb
index ef09d9505d2..c570eace862 100644
--- a/lib/api/milestone_responses.rb
+++ b/lib/api/milestone_responses.rb
@@ -28,7 +28,7 @@ module API
end
def list_milestones_for(parent)
- milestones = parent.milestones
+ milestones = parent.milestones.order_id_desc
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]
diff --git a/lib/api/v3/merge_request_diffs.rb b/lib/api/v3/merge_request_diffs.rb
index 35f462e907b..22866fc2845 100644
--- a/lib/api/v3/merge_request_diffs.rb
+++ b/lib/api/v3/merge_request_diffs.rb
@@ -20,7 +20,7 @@ module API
get ":id/merge_requests/:merge_request_id/versions" do
merge_request = find_merge_request_with_access(params[:merge_request_id])
- present merge_request.merge_request_diffs, with: ::API::Entities::MergeRequestDiff
+ present merge_request.merge_request_diffs.order_id_desc, with: ::API::Entities::MergeRequestDiff
end
desc 'Get a single merge request diff version' do
diff --git a/lib/api/v3/milestones.rb b/lib/api/v3/milestones.rb
index 4c7061d4939..9be4cf9d22a 100644
--- a/lib/api/v3/milestones.rb
+++ b/lib/api/v3/milestones.rb
@@ -34,6 +34,7 @@ module API
milestones = user_project.milestones
milestones = filter_milestones_state(milestones, params[:state])
milestones = filter_by_iid(milestones, params[:iid]) if params[:iid].present?
+ milestones = milestones.order_id_desc
present paginate(milestones), with: ::API::Entities::Milestone
end
diff --git a/lib/api/v3/projects.rb b/lib/api/v3/projects.rb
index 74df246bdfe..7c260b8d910 100644
--- a/lib/api/v3/projects.rb
+++ b/lib/api/v3/projects.rb
@@ -120,7 +120,7 @@ module API
get do
authenticate!
- present_projects current_user.authorized_projects,
+ present_projects current_user.authorized_projects.order_id_desc,
with: ::API::V3::Entities::ProjectWithAccess
end
diff --git a/lib/banzai/filter/sanitization_filter.rb b/lib/banzai/filter/sanitization_filter.rb
index 2d6e8ffc90f..9923ec4e870 100644
--- a/lib/banzai/filter/sanitization_filter.rb
+++ b/lib/banzai/filter/sanitization_filter.rb
@@ -5,6 +5,7 @@ module Banzai
# Extends HTML::Pipeline::SanitizationFilter with a custom whitelist.
class SanitizationFilter < HTML::Pipeline::SanitizationFilter
UNSAFE_PROTOCOLS = %w(data javascript vbscript).freeze
+ TABLE_ALIGNMENT_PATTERN = /text-align: (?<alignment>center|left|right)/
def whitelist
whitelist = super
@@ -24,7 +25,8 @@ module Banzai
# Only push these customizations once
return if customized?(whitelist[:transformers])
- # Allow table alignment
+ # Allow table alignment; we whitelist specific style properties in a
+ # transformer below
whitelist[:attributes]['th'] = %w(style)
whitelist[:attributes]['td'] = %w(style)
@@ -43,6 +45,9 @@ module Banzai
whitelist[:elements].push('abbr')
whitelist[:attributes]['abbr'] = %w(title)
+ # Disallow `name` attribute globally
+ whitelist[:attributes][:all].delete('name')
+
# Allow any protocol in `a` elements...
whitelist[:protocols].delete('a')
@@ -52,6 +57,9 @@ module Banzai
# Remove `rel` attribute from `a` elements
whitelist[:transformers].push(self.class.remove_rel)
+ # Remove any `style` properties not required for table alignment
+ whitelist[:transformers].push(self.class.remove_unsafe_table_style)
+
whitelist
end
@@ -81,6 +89,21 @@ module Banzai
end
end
end
+
+ def remove_unsafe_table_style
+ lambda do |env|
+ node = env[:node]
+
+ return unless node.name == 'th' || node.name == 'td'
+ return unless node.has_attribute?('style')
+
+ if node['style'] =~ TABLE_ALIGNMENT_PATTERN
+ node['style'] = "text-align: #{$~[:alignment]}"
+ else
+ node.remove_attribute('style')
+ end
+ end
+ end
end
end
end
diff --git a/lib/gitlab/exclusive_lease.rb b/lib/gitlab/exclusive_lease.rb
index 3784f6c4947..3f7b42456af 100644
--- a/lib/gitlab/exclusive_lease.rb
+++ b/lib/gitlab/exclusive_lease.rb
@@ -25,6 +25,12 @@ module Gitlab
end
EOS
+ def self.get_uuid(key)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.get(redis_shared_state_key(key)) || false
+ end
+ end
+
def self.cancel(key, uuid)
Gitlab::Redis::SharedState.with do |redis|
redis.eval(LUA_CANCEL_SCRIPT, keys: [redis_shared_state_key(key)], argv: [uuid])
@@ -35,10 +41,10 @@ module Gitlab
"gitlab:exclusive_lease:#{key}"
end
- def initialize(key, timeout:)
+ def initialize(key, uuid: nil, timeout:)
@redis_shared_state_key = self.class.redis_shared_state_key(key)
@timeout = timeout
- @uuid = SecureRandom.uuid
+ @uuid = uuid || SecureRandom.uuid
end
# Try to obtain the lease. Return lease UUID on success,
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 9a5f4f598b2..a3dc2cd0b60 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -70,21 +70,41 @@ module Gitlab
params['gitaly_token'].presence || Gitlab.config.gitaly['token']
end
- def self.feature_enabled?(feature, status: MigrationStatus::OPT_IN)
+ # Evaluates whether a feature toggle is on or off
+ def self.feature_enabled?(feature_name, status: MigrationStatus::OPT_IN)
+ # Disabled features are always off!
return false if status == MigrationStatus::DISABLED
- feature = Feature.get("gitaly_#{feature}")
+ feature = Feature.get("gitaly_#{feature_name}")
- # If the feature hasn't been set, turn it on if it's opt-out
- return status == MigrationStatus::OPT_OUT unless Feature.persisted?(feature)
+ # If the feature has been set, always evaluate
+ if Feature.persisted?(feature)
+ if feature.percentage_of_time_value > 0
+ # Probabilistically enable this feature
+ return Random.rand() * 100 < feature.percentage_of_time_value
+ end
+
+ return feature.enabled?
+ end
- if feature.percentage_of_time_value > 0
- # Probabilistically enable this feature
- return Random.rand() * 100 < feature.percentage_of_time_value
+ # If the feature has not been set, the default depends
+ # on it's status
+ case status
+ when MigrationStatus::OPT_OUT
+ true
+ when MigrationStatus::OPT_IN
+ opt_into_all_features?
+ else
+ false
end
+ end
- feature.enabled?
+ # opt_into_all_features? returns true when the current environment
+ # is one in which we opt into features automatically
+ def self.opt_into_all_features?
+ Rails.env.development? || ENV["GITALY_FEATURE_DEFAULT_ON"] == "1"
end
+ private_class_method :opt_into_all_features?
def self.migrate(feature, status: MigrationStatus::OPT_IN)
is_enabled = feature_enabled?(feature, status: status)
diff --git a/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
new file mode 100644
index 00000000000..1e1fdabca93
--- /dev/null
+++ b/lib/gitlab/grape_logging/formatters/lograge_with_timestamp.rb
@@ -0,0 +1,19 @@
+module Gitlab
+ module GrapeLogging
+ module Formatters
+ class LogrageWithTimestamp
+ def call(severity, datetime, _, data)
+ time = data.delete :time
+ attributes = {
+ time: datetime.utc.iso8601(3),
+ severity: severity,
+ duration: time[:total],
+ db: time[:db],
+ view: time[:view]
+ }.merge(data)
+ ::Lograge.formatter.call(attributes) + "\n"
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/import_export/import_export.yml b/lib/gitlab/import_export/import_export.yml
index ec73846d844..2171c6c7bbb 100644
--- a/lib/gitlab/import_export/import_export.yml
+++ b/lib/gitlab/import_export/import_export.yml
@@ -50,6 +50,7 @@ project_tree:
- :push_event_payload
- :stages
- :statuses
+ - :auto_devops
- :triggers
- :pipeline_schedules
- :services
@@ -142,4 +143,4 @@ methods:
events:
- :action
push_event_payload:
- - :action \ No newline at end of file
+ - :action
diff --git a/lib/gitlab/import_export/relation_factory.rb b/lib/gitlab/import_export/relation_factory.rb
index d563a87dcfd..380b336395d 100644
--- a/lib/gitlab/import_export/relation_factory.rb
+++ b/lib/gitlab/import_export/relation_factory.rb
@@ -14,6 +14,7 @@ module Gitlab
create_access_levels: 'ProtectedTag::CreateAccessLevel',
labels: :project_labels,
priorities: :label_priorities,
+ auto_devops: :project_auto_devops,
label: :project_label }.freeze
USER_REFERENCES = %w[author_id assignee_id updated_by_id user_id created_by_id last_edited_by_id merge_user_id resolved_by_id].freeze
diff --git a/lib/gitlab/middleware/go.rb b/lib/gitlab/middleware/go.rb
index 6023fa1820f..f42168c720e 100644
--- a/lib/gitlab/middleware/go.rb
+++ b/lib/gitlab/middleware/go.rb
@@ -3,6 +3,10 @@
module Gitlab
module Middleware
class Go
+ include ActionView::Helpers::TagHelper
+
+ PROJECT_PATH_REGEX = %r{\A(#{Gitlab::PathRegex.full_namespace_route_regex}/#{Gitlab::PathRegex.project_route_regex})/}.freeze
+
def initialize(app)
@app = app
end
@@ -10,17 +14,20 @@ module Gitlab
def call(env)
request = Rack::Request.new(env)
- if go_request?(request)
- render_go_doc(request)
- else
- @app.call(env)
- end
+ render_go_doc(request) || @app.call(env)
end
private
def render_go_doc(request)
- body = go_body(request)
+ return unless go_request?(request)
+
+ path = project_path(request)
+ return unless path
+
+ body = go_body(path)
+ return unless body
+
response = Rack::Response.new(body, 200, { 'Content-Type' => 'text/html' })
response.finish
end
@@ -29,11 +36,13 @@ module Gitlab
request["go-get"].to_i == 1 && request.env["PATH_INFO"].present?
end
- def go_body(request)
- project_url = URI.join(Gitlab.config.gitlab.url, project_path(request))
+ def go_body(path)
+ project_url = URI.join(Gitlab.config.gitlab.url, path)
import_prefix = strip_url(project_url.to_s)
- "<!DOCTYPE html><html><head><meta content='#{import_prefix} git #{project_url}.git' name='go-import'></head></html>\n"
+ meta_tag = tag :meta, name: 'go-import', content: "#{import_prefix} git #{project_url}.git"
+ head_tag = content_tag :head, meta_tag
+ content_tag :html, head_tag
end
def strip_url(url)
@@ -44,6 +53,10 @@ module Gitlab
path_info = request.env["PATH_INFO"]
path_info.sub!(/^\//, '')
+ project_path_match = "#{path_info}/".match(PROJECT_PATH_REGEX)
+ return unless project_path_match
+ path = project_path_match[1]
+
# Go subpackages may be in the form of `namespace/project/path1/path2/../pathN`.
# In a traditional project with a single namespace, this would denote repo
# `namespace/project` with subpath `path1/path2/../pathN`, but with nested
@@ -51,7 +64,7 @@ module Gitlab
# `path2/../pathN`, for example.
# We find all potential project paths out of the path segments
- path_segments = path_info.split('/')
+ path_segments = path.split('/')
simple_project_path = path_segments.first(2).join('/')
# If the path is at most 2 segments long, it is a simple `namespace/project` path and we're done
diff --git a/lib/gitlab/themes.rb b/lib/gitlab/themes.rb
new file mode 100644
index 00000000000..d43eff5ba4a
--- /dev/null
+++ b/lib/gitlab/themes.rb
@@ -0,0 +1,84 @@
+module Gitlab
+ # Module containing GitLab's application theme definitions and helper methods
+ # for accessing them.
+ module Themes
+ extend self
+
+ # Theme ID used when no `default_theme` configuration setting is provided.
+ APPLICATION_DEFAULT = 1
+
+ # Struct class representing a single Theme
+ Theme = Struct.new(:id, :name, :css_class)
+
+ # All available Themes
+ THEMES = [
+ Theme.new(1, 'Indigo', 'ui_indigo'),
+ Theme.new(2, 'Dark', 'ui_dark'),
+ Theme.new(3, 'Light', 'ui_light'),
+ Theme.new(4, 'Blue', 'ui_blue'),
+ Theme.new(5, 'Green', 'ui_green')
+ ].freeze
+
+ # Convenience method to get a space-separated String of all the theme
+ # classes that might be applied to the `body` element
+ #
+ # Returns a String
+ def body_classes
+ THEMES.collect(&:css_class).uniq.join(' ')
+ end
+
+ # Get a Theme by its ID
+ #
+ # If the ID is invalid, returns the default Theme.
+ #
+ # id - Integer ID
+ #
+ # Returns a Theme
+ def by_id(id)
+ THEMES.detect { |t| t.id == id } || default
+ end
+
+ # Returns the number of defined Themes
+ def count
+ THEMES.size
+ end
+
+ # Get the default Theme
+ #
+ # Returns a Theme
+ def default
+ by_id(default_id)
+ end
+
+ # Iterate through each Theme
+ #
+ # Yields the Theme object
+ def each(&block)
+ THEMES.each(&block)
+ end
+
+ # Get the Theme for the specified user, or the default
+ #
+ # user - User record
+ #
+ # Returns a Theme
+ def for_user(user)
+ if user
+ by_id(user.theme_id)
+ else
+ default
+ end
+ end
+
+ private
+
+ def default_id
+ @default_id ||= begin
+ id = Gitlab.config.gitlab.default_theme.to_i
+ theme_ids = THEMES.map(&:id)
+
+ theme_ids.include?(id) ? id : APPLICATION_DEFAULT
+ end
+ end
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index e5cf2aeb513..bd3d5b7c839 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -987,6 +987,24 @@ msgstr ""
msgid "Push Rules"
msgstr ""
+msgid "ProjectsDropdown|Loading projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Sorry, no projects matched your search"
+msgstr ""
+
+msgid "ProjectsDropdown|Projects you visit often will appear here"
+msgstr ""
+
+msgid "ProjectsDropdown|Search your projects"
+msgstr ""
+
+msgid "ProjectsDropdown|Something went wrong on our end"
+msgstr ""
+
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "Push events"
msgstr ""
diff --git a/scripts/trigger-build-docs b/scripts/trigger-build-docs
new file mode 100755
index 00000000000..44f832ed3e6
--- /dev/null
+++ b/scripts/trigger-build-docs
@@ -0,0 +1,80 @@
+#!/usr/bin/env ruby
+
+require 'gitlab'
+
+#
+# Give the remote branch a different name than the current one
+# in order to avoid conflicts
+#
+@docs_branch = "#{ENV["CI_COMMIT_REF_SLUG"]}-built-from-ce-ee"
+GITLAB_DOCS_REPO = 'gitlab-com/gitlab-docs'.freeze
+
+#
+# Configure credentials to be used with gitlab gem
+#
+Gitlab.configure do |config|
+ config.endpoint = 'https://gitlab.com/api/v4'
+ config.private_token = ENV["DOCS_API_TOKEN"] # GitLab Docs bot access token which has only Developer access to gitlab-docs
+end
+
+#
+# Dummy way to find out in which repo we are, CE or EE
+#
+def ee?
+ File.exist?('CHANGELOG-EE.md')
+end
+
+#
+# Create a remote branch in gitlab-docs
+#
+def create_remote_branch
+ Gitlab.create_branch(GITLAB_DOCS_REPO, @docs_branch, 'master')
+ puts "Remote branch '#{@docs_branch}' created"
+rescue Gitlab::Error::BadRequest
+ puts "Remote branch '#{@docs_branch}' already exists"
+end
+
+#
+# Remove a remote branch in gitlab-docs
+#
+def remove_remote_branch
+ Gitlab.delete_branch(GITLAB_DOCS_REPO, @docs_branch)
+ puts "Remote branch '#{@docs_branch}' deleted"
+end
+
+#
+# Trigger a pipeline in gitlab-docs
+#
+def trigger_pipeline
+ # Overriding vars in https://gitlab.com/gitlab-com/gitlab-docs/blob/master/.gitlab-ci.yml
+ param_name = ee? ? 'BRANCH_EE' : 'BRANCH_CE'
+
+ # The review app URL
+ app_url = "http://#{@docs_branch}.#{ENV["DOCS_REVIEW_APPS_DOMAIN"]}/#{ee? ? 'ee' : 'ce'}"
+
+ # Create the pipeline
+ puts "=> Triggering a pipeline..."
+ pipeline = Gitlab.run_trigger(GITLAB_DOCS_REPO, ENV["DOCS_TRIGGER_TOKEN"], @docs_branch, { param_name => ENV["CI_COMMIT_REF_NAME"] })
+
+ puts "=> Pipeline created:"
+ puts ""
+ puts "https://gitlab.com/gitlab-com/gitlab-docs/pipelines/#{pipeline.id}"
+ puts ""
+ puts "=> Preview your changes live at:"
+ puts ""
+ puts app_url
+ puts ""
+end
+
+#
+# When the first argument is deploy then create the branch and trigger pipeline
+# When it is 'stop', it deleted the remote branch. That way, we ensure there
+# are no stale remote branches and the Review server doesn't fill.
+#
+case ARGV[0]
+when 'deploy'
+ create_remote_branch
+ trigger_pipeline
+when 'cleanup'
+ remove_remote_branch
+end
diff --git a/scripts/trigger-build b/scripts/trigger-build-omnibus
index dcda70d7ed8..dcda70d7ed8 100755
--- a/scripts/trigger-build
+++ b/scripts/trigger-build-omnibus
diff --git a/spec/controllers/groups_controller_spec.rb b/spec/controllers/groups_controller_spec.rb
index c2ada8c8df7..b0564e27a68 100644
--- a/spec/controllers/groups_controller_spec.rb
+++ b/spec/controllers/groups_controller_spec.rb
@@ -2,9 +2,133 @@ require 'rails_helper'
describe GroupsController do
let(:user) { create(:user) }
+ let(:admin) { create(:admin) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, namespace: group) }
let!(:group_member) { create(:group_member, group: group, user: user) }
+ let!(:owner) { group.add_owner(create(:user)).user }
+ let!(:master) { group.add_master(create(:user)).user }
+ let!(:developer) { group.add_developer(create(:user)).user }
+ let!(:guest) { group.add_guest(create(:user)).user }
+
+ shared_examples 'member with ability to create subgroups' do
+ it 'renders the new page' do
+ sign_in(member)
+
+ get :new, parent_id: group.id
+
+ expect(response).to render_template(:new)
+ end
+ end
+
+ shared_examples 'member without ability to create subgroups' do
+ it 'renders the 404 page' do
+ sign_in(member)
+
+ get :new, parent_id: group.id
+
+ expect(response).not_to render_template(:new)
+ expect(response.status).to eq(404)
+ end
+ end
+
+ describe 'GET #new' do
+ context 'when creating subgroups', :nested_groups do
+ [true, false].each do |can_create_group_status|
+ context "and can_create_group is #{can_create_group_status}" do
+ before do
+ User.where(id: [admin, owner, master, developer, guest]).update_all(can_create_group: can_create_group_status)
+ end
+
+ [:admin, :owner].each do |member_type|
+ context "and logged in as #{member_type.capitalize}" do
+ it_behaves_like 'member with ability to create subgroups' do
+ let(:member) { send(member_type) }
+ end
+ end
+ end
+
+ [:guest, :developer, :master].each do |member_type|
+ context "and logged in as #{member_type.capitalize}" do
+ it_behaves_like 'member without ability to create subgroups' do
+ let(:member) { send(member_type) }
+ end
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST #create' do
+ context 'when creating subgroups', :nested_groups do
+ [true, false].each do |can_create_group_status|
+ context "and can_create_group is #{can_create_group_status}" do
+ context 'and logged in as Owner' do
+ it 'creates the subgroup' do
+ owner.update_attribute(:can_create_group, can_create_group_status)
+ sign_in(owner)
+
+ post :create, group: { parent_id: group.id, path: 'subgroup' }
+
+ expect(response).to be_redirect
+ expect(response.body).to match(%r{http://test.host/#{group.path}/subgroup})
+ end
+ end
+
+ context 'and logged in as Developer' do
+ it 'renders the new template' do
+ developer.update_attribute(:can_create_group, can_create_group_status)
+ sign_in(developer)
+
+ previous_group_count = Group.count
+
+ post :create, group: { parent_id: group.id, path: 'subgroup' }
+
+ expect(response).to render_template(:new)
+ expect(Group.count).to eq(previous_group_count)
+ end
+ end
+ end
+ end
+ end
+
+ context 'when creating a top level group' do
+ before do
+ sign_in(developer)
+ end
+
+ context 'and can_create_group is enabled' do
+ before do
+ developer.update_attribute(:can_create_group, true)
+ end
+
+ it 'creates the Group' do
+ original_group_count = Group.count
+
+ post :create, group: { path: 'subgroup' }
+
+ expect(Group.count).to eq(original_group_count + 1)
+ expect(response).to be_redirect
+ end
+ end
+
+ context 'and can_create_group is disabled' do
+ before do
+ developer.update_attribute(:can_create_group, false)
+ end
+
+ it 'does not create the Group' do
+ original_group_count = Group.count
+
+ post :create, group: { path: 'subgroup' }
+
+ expect(Group.count).to eq(original_group_count)
+ expect(response).to render_template(:new)
+ end
+ end
+ end
+ end
describe 'GET #index' do
context 'as a user' do
diff --git a/spec/controllers/profiles/preferences_controller_spec.rb b/spec/controllers/profiles/preferences_controller_spec.rb
index a5f544b4f92..a66b4ab0902 100644
--- a/spec/controllers/profiles/preferences_controller_spec.rb
+++ b/spec/controllers/profiles/preferences_controller_spec.rb
@@ -25,7 +25,8 @@ describe Profiles::PreferencesController do
def go(params: {}, format: :js)
params.reverse_merge!(
color_scheme_id: '1',
- dashboard: 'stars'
+ dashboard: 'stars',
+ theme_id: '1'
)
patch :update, user: params, format: format
@@ -40,7 +41,8 @@ describe Profiles::PreferencesController do
it "changes the user's preferences" do
prefs = {
color_scheme_id: '1',
- dashboard: 'stars'
+ dashboard: 'stars',
+ theme_id: '2'
}.with_indifferent_access
expect(user).to receive(:assign_attributes).with(prefs)
diff --git a/spec/factories/project_auto_devops.rb b/spec/factories/project_auto_devops.rb
new file mode 100644
index 00000000000..8d124dc2381
--- /dev/null
+++ b/spec/factories/project_auto_devops.rb
@@ -0,0 +1,7 @@
+FactoryGirl.define do
+ factory :project_auto_devops do
+ project
+ enabled true
+ domain "example.com"
+ end
+end
diff --git a/spec/features/copy_as_gfm_spec.rb b/spec/features/copy_as_gfm_spec.rb
index 3e6a27eafd8..dfeba722ac6 100644
--- a/spec/features/copy_as_gfm_spec.rb
+++ b/spec/features/copy_as_gfm_spec.rb
@@ -288,8 +288,6 @@ describe 'Copy as GFM', js: true do
'SanitizationFilter',
<<-GFM.strip_heredoc
- <a name="named-anchor"></a>
-
<sub>sub</sub>
<dl>
diff --git a/spec/features/groups/share_lock_spec.rb b/spec/features/groups/share_lock_spec.rb
new file mode 100644
index 00000000000..8842d1391aa
--- /dev/null
+++ b/spec/features/groups/share_lock_spec.rb
@@ -0,0 +1,62 @@
+require 'spec_helper'
+
+feature 'Group share with group lock' do
+ given(:root_owner) { create(:user) }
+ given(:root_group) { create(:group) }
+
+ background do
+ root_group.add_owner(root_owner)
+ sign_in(root_owner)
+ end
+
+ context 'with a subgroup', :nested_groups do
+ given!(:subgroup) { create(:group, parent: root_group) }
+
+ context 'when enabling the parent group share with group lock' do
+ scenario 'the subgroup share with group lock becomes enabled' do
+ visit edit_group_path(root_group)
+ check 'group_share_with_group_lock'
+
+ click_on 'Save group'
+
+ expect(subgroup.reload.share_with_group_lock?).to be_truthy
+ end
+ end
+
+ context 'when disabling the parent group share with group lock (which was already enabled)' do
+ background do
+ visit edit_group_path(root_group)
+ check 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+
+ context 'and the subgroup share with group lock is enabled' do
+ scenario 'the subgroup share with group lock does not change' do
+ visit edit_group_path(root_group)
+ uncheck 'group_share_with_group_lock'
+
+ click_on 'Save group'
+
+ expect(subgroup.reload.share_with_group_lock?).to be_truthy
+ end
+ end
+
+ context 'but the subgroup share with group lock is disabled' do
+ background do
+ visit edit_group_path(subgroup)
+ uncheck 'group_share_with_group_lock'
+ click_on 'Save group'
+ end
+
+ scenario 'the subgroup share with group lock does not change' do
+ visit edit_group_path(root_group)
+ uncheck 'group_share_with_group_lock'
+
+ click_on 'Save group'
+
+ expect(subgroup.reload.share_with_group_lock?).to be_falsey
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/edit_spec.rb b/spec/features/projects/edit_spec.rb
index d3b1d1f7be3..17f914c9c17 100644
--- a/spec/features/projects/edit_spec.rb
+++ b/spec/features/projects/edit_spec.rb
@@ -1,20 +1,21 @@
require 'rails_helper'
feature 'Project edit', js: true do
+ let(:admin) { create(:admin) }
let(:user) { create(:user) }
let(:project) { create(:project) }
- before do
- project.team << [user, :master]
- sign_in(user)
+ context 'feature visibility' do
+ before do
+ project.team << [user, :master]
+ sign_in(user)
- visit edit_project_path(project)
- end
+ visit edit_project_path(project)
+ end
- context 'feature visibility' do
context 'merge requests select' do
it 'hides merge requests section' do
- select('Disabled', from: 'project_project_feature_attributes_merge_requests_access_level')
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
expect(page).to have_selector('.merge-requests-feature', visible: false)
end
@@ -30,7 +31,7 @@ feature 'Project edit', js: true do
context 'builds select' do
it 'hides builds select section' do
- select('Disabled', from: 'project_project_feature_attributes_builds_access_level')
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
expect(page).to have_selector('.builds-feature', visible: false)
end
@@ -44,4 +45,18 @@ feature 'Project edit', js: true do
end
end
end
+
+ context 'LFS enabled setting' do
+ before do
+ sign_in(admin)
+ end
+
+ it 'displays the correct elements' do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ visit edit_project_path(project)
+
+ expect(page).to have_content('Git Large File Storage')
+ expect(page).to have_selector('input[name="project[lfs_enabled]"] + button', visible: true)
+ end
+ end
end
diff --git a/spec/features/projects/environments/environments_spec.rb b/spec/features/projects/environments/environments_spec.rb
index 1c59e57c0a4..af7ad365546 100644
--- a/spec/features/projects/environments/environments_spec.rb
+++ b/spec/features/projects/environments/environments_spec.rb
@@ -10,26 +10,23 @@ feature 'Environments page', :js do
sign_in(user)
end
- given!(:environment) { }
- given!(:deployment) { }
- given!(:action) { }
-
- before do
- visit_environments(project)
- end
-
describe 'page tabs' do
- scenario 'shows "Available" and "Stopped" tab with links' do
+ it 'shows "Available" and "Stopped" tab with links' do
+ visit_environments(project)
+
expect(page).to have_link('Available')
expect(page).to have_link('Stopped')
end
describe 'with one available environment' do
- given(:environment) { create(:environment, project: project, state: :available) }
+ before do
+ create(:environment, project: project, state: :available)
+ end
describe 'in available tab page' do
it 'should show one environment' do
- visit project_environments_path(project, scope: 'available')
+ visit_environments(project, scope: 'available')
+
expect(page).to have_css('.environments-container')
expect(page.all('.environment-name').length).to eq(1)
end
@@ -37,7 +34,8 @@ feature 'Environments page', :js do
describe 'in stopped tab page' do
it 'should show no environments' do
- visit project_environments_path(project, scope: 'stopped')
+ visit_environments(project, scope: 'stopped')
+
expect(page).to have_css('.environments-container')
expect(page).to have_content('You don\'t have any environments right now')
end
@@ -45,11 +43,14 @@ feature 'Environments page', :js do
end
describe 'with one stopped environment' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
+ before do
+ create(:environment, project: project, state: :stopped)
+ end
describe 'in available tab page' do
it 'should show no environments' do
- visit project_environments_path(project, scope: 'available')
+ visit_environments(project, scope: 'available')
+
expect(page).to have_css('.environments-container')
expect(page).to have_content('You don\'t have any environments right now')
end
@@ -57,7 +58,8 @@ feature 'Environments page', :js do
describe 'in stopped tab page' do
it 'should show one environment' do
- visit project_environments_path(project, scope: 'stopped')
+ visit_environments(project, scope: 'stopped')
+
expect(page).to have_css('.environments-container')
expect(page.all('.environment-name').length).to eq(1)
end
@@ -66,86 +68,84 @@ feature 'Environments page', :js do
end
context 'without environments' do
- scenario 'does show no environments' do
- expect(page).to have_content('You don\'t have any environments right now.')
+ before do
+ visit_environments(project)
end
- scenario 'does show 0 as counter for environments in both tabs' do
+ it 'does not show environments and counters are set to zero' do
+ expect(page).to have_content('You don\'t have any environments right now.')
+
expect(page.find('.js-available-environments-count').text).to eq('0')
expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
end
- describe 'when showing the environment' do
- given(:environment) { create(:environment, project: project) }
-
- scenario 'does show environment name' do
- expect(page).to have_link(environment.name)
- end
-
- scenario 'does show number of available and stopped environments' do
- expect(page.find('.js-available-environments-count').text).to eq('1')
- expect(page.find('.js-stopped-environments-count').text).to eq('0')
+ describe 'environments table' do
+ given!(:environment) do
+ create(:environment, project: project, state: :available)
end
- context 'without deployments' do
- scenario 'does show no deployments' do
- expect(page).to have_content('No deployments yet')
+ context 'when there are no deployments' do
+ before do
+ visit_environments(project)
end
- context 'for available environment' do
- given(:environment) { create(:environment, project: project, state: :available) }
+ it 'shows environments names and counters' do
+ expect(page).to have_link(environment.name)
- scenario 'does not shows stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
+ expect(page.find('.js-available-environments-count').text).to eq('1')
+ expect(page.find('.js-stopped-environments-count').text).to eq('0')
end
- context 'for stopped environment' do
- given(:environment) { create(:environment, project: project, state: :stopped) }
+ it 'does not show deployments' do
+ expect(page).to have_content('No deployments yet')
+ end
- scenario 'does not shows stop button' do
- expect(page).not_to have_selector('.stop-env-link')
- end
+ it 'does not show stip button when environment is not stoppable' do
+ expect(page).not_to have_selector('.stop-env-link')
end
end
- context 'with deployments' do
+ context 'when there are deployments' do
given(:project) { create(:project, :repository) }
- given(:deployment) do
+ given!(:deployment) do
create(:deployment, environment: environment,
sha: project.commit.id)
end
- scenario 'does show deployment SHA' do
- expect(page).to have_link(deployment.short_sha)
- end
+ it 'shows deployment SHA and internal ID' do
+ visit_environments(project)
- scenario 'does show deployment internal id' do
+ expect(page).to have_link(deployment.short_sha)
expect(page).to have_content(deployment.iid)
end
- context 'with build and manual actions' do
- given(:pipeline) { create(:ci_pipeline, project: project) }
- given(:build) { create(:ci_build, pipeline: pipeline) }
+ context 'when builds and manual actions are present' do
+ given!(:pipeline) { create(:ci_pipeline, project: project) }
+ given!(:build) { create(:ci_build, pipeline: pipeline) }
- given(:action) do
+ given!(:action) do
create(:ci_build, :manual, pipeline: pipeline, name: 'deploy to production')
end
- given(:deployment) do
+ given!(:deployment) do
create(:deployment, environment: environment,
deployable: build,
sha: project.commit.id)
end
- scenario 'does show a play button' do
+ before do
+ visit_environments(project)
+ end
+
+ it 'shows a play button' do
find('.js-dropdown-play-icon-container').click
+
expect(page).to have_content(action.name.humanize)
end
- scenario 'does allow to play manual action', js: true do
+ it 'allows to play a manual action', js: true do
expect(action).to be_manual
find('.js-dropdown-play-icon-container').click
@@ -155,19 +155,19 @@ feature 'Environments page', :js do
.not_to change { Ci::Pipeline.count }
end
- scenario 'does show build name and id' do
+ it 'shows build name and id' do
expect(page).to have_link("#{build.name} ##{build.id}")
end
- scenario 'does not show stop button' do
+ it 'shows a stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
- scenario 'does not show external link button' do
+ it 'does not show external link button' do
expect(page).not_to have_css('external-url')
end
- scenario 'does not show terminal button' do
+ it 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
@@ -176,7 +176,7 @@ feature 'Environments page', :js do
given(:build) { create(:ci_build, pipeline: pipeline) }
given(:deployment) { create(:deployment, environment: environment, deployable: build) }
- scenario 'does show an external link button' do
+ it 'shows an external link button' do
expect(page).to have_link(nil, href: environment.external_url)
end
end
@@ -192,34 +192,34 @@ feature 'Environments page', :js do
on_stop: 'close_app')
end
- scenario 'does show stop button' do
+ it 'shows a stop button' do
expect(page).to have_selector('.stop-env-link')
end
- context 'for reporter' do
+ context 'when user is a reporter' do
let(:role) { :reporter }
- scenario 'does not show stop button' do
+ it 'does not show stop button' do
expect(page).not_to have_selector('.stop-env-link')
end
end
end
- context 'with terminal' do
+ context 'when kubernetes terminal is available' do
let(:project) { create(:kubernetes_project, :test_repo) }
context 'for project master' do
let(:role) { :master }
- scenario 'it shows the terminal button' do
+ it 'shows the terminal button' do
expect(page).to have_terminal_button
end
end
- context 'for developer' do
+ context 'when user is a developer' do
let(:role) { :developer }
- scenario 'does not show terminal button' do
+ it 'does not show terminal button' do
expect(page).not_to have_terminal_button
end
end
@@ -228,59 +228,77 @@ feature 'Environments page', :js do
end
end
- scenario 'does have a New environment button' do
+ it 'does have a new environment button' do
+ visit_environments(project)
+
expect(page).to have_link('New environment')
end
- describe 'when creating a new environment' do
+ describe 'creating a new environment' do
before do
visit_environments(project)
end
- context 'when logged as developer' do
- before do
- within(".top-area") do
- click_link 'New environment'
- end
- end
+ context 'user is a developer' do
+ given(:role) { :developer }
- context 'for valid name' do
- before do
- fill_in('Name', with: 'production')
- click_on 'Save'
- end
+ scenario 'developer creates a new environment with a valid name' do
+ within(".top-area") { click_link 'New environment' }
+ fill_in('Name', with: 'production')
+ click_on 'Save'
- scenario 'does create a new pipeline' do
- expect(page).to have_content('production')
- end
+ expect(page).to have_content('production')
end
- context 'for invalid name' do
- before do
- fill_in('Name', with: 'name,with,commas')
- click_on 'Save'
- end
+ scenario 'developer creates a new environmetn with invalid name' do
+ within(".top-area") { click_link 'New environment' }
+ fill_in('Name', with: 'name,with,commas')
+ click_on 'Save'
- scenario 'does show errors' do
- expect(page).to have_content('Name can contain only letters')
- end
+ expect(page).to have_content('Name can contain only letters')
end
end
- context 'when logged as reporter' do
+ context 'user is a reporter' do
given(:role) { :reporter }
- scenario 'does not have a New environment link' do
+ scenario 'reporters tries to create a new environment' do
expect(page).not_to have_link('New environment')
end
end
end
+ describe 'environments folders' do
+ before do
+ create(:environment, project: project,
+ name: 'staging/review-1',
+ state: :available)
+ create(:environment, project: project,
+ name: 'staging/review-2',
+ state: :available)
+ end
+
+ scenario 'users unfurls an environment folder' do
+ visit_environments(project)
+
+ expect(page).not_to have_content 'review-1'
+ expect(page).not_to have_content 'review-2'
+ expect(page).to have_content 'staging 2'
+
+ within('.folder-row') do
+ find('.folder-name', text: 'staging').click
+ end
+
+ expect(page).to have_content 'review-1'
+ expect(page).to have_content 'review-2'
+ end
+ end
+
def have_terminal_button
have_link(nil, href: terminal_project_environment_path(project, environment))
end
- def visit_environments(project)
- visit project_environments_path(project)
+ def visit_environments(project, **opts)
+ visit project_environments_path(project, **opts)
end
end
diff --git a/spec/features/projects/features_visibility_spec.rb b/spec/features/projects/features_visibility_spec.rb
index 24691629063..57722276d79 100644
--- a/spec/features/projects/features_visibility_spec.rb
+++ b/spec/features/projects/features_visibility_spec.rb
@@ -19,21 +19,16 @@ describe 'Edit Project Settings' do
it 'toggles visibility' do
visit edit_project_path(project)
- select 'Disabled', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ # disable by clicking toggle
+ toggle_feature_off("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
click_button 'Save changes'
end
wait_for_requests
expect(page).not_to have_selector(".shortcuts-#{shortcut_name}")
- select 'Everyone with access', from: "project_project_feature_attributes_#{tool_name}_access_level"
- page.within('.sharing-permissions') do
- click_button 'Save changes'
- end
- wait_for_requests
- expect(page).to have_selector(".shortcuts-#{shortcut_name}")
-
- select 'Only team members', from: "project_project_feature_attributes_#{tool_name}_access_level"
+ # re-enable by clicking toggle again
+ toggle_feature_on("project[project_feature_attributes][#{tool_name}_access_level]")
page.within('.sharing-permissions') do
click_button 'Save changes'
end
@@ -176,19 +171,19 @@ describe 'Edit Project Settings' do
end
it "disables repository related features" do
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
page.within('.sharing-permissions') do
click_button "Save changes"
end
- expect(find(".sharing-permissions")).to have_selector("select.disabled", count: 2)
+ expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.disabled", count: 2)
end
it "shows empty features project homepage" do
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
- select "Disabled", from: "project_project_feature_attributes_issues_access_level"
- select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('project[project_feature_attributes][issues_access_level]')
+ toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
page.within('.sharing-permissions') do
click_button "Save changes"
@@ -201,9 +196,9 @@ describe 'Edit Project Settings' do
end
it "hides project activity tabs" do
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
- select "Disabled", from: "project_project_feature_attributes_issues_access_level"
- select "Disabled", from: "project_project_feature_attributes_wiki_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
+ toggle_feature_off('project[project_feature_attributes][issues_access_level]')
+ toggle_feature_off('project[project_feature_attributes][wiki_access_level]')
page.within('.sharing-permissions') do
click_button "Save changes"
@@ -222,7 +217,7 @@ describe 'Edit Project Settings' do
# Regression spec for https://gitlab.com/gitlab-org/gitlab-ce/issues/25272
it "hides comments activity tab only on disabled issues, merge requests and repository" do
- select "Disabled", from: "project_project_feature_attributes_issues_access_level"
+ toggle_feature_off('project[project_feature_attributes][issues_access_level]')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -230,7 +225,7 @@ describe 'Edit Project Settings' do
visit edit_project_path(project)
- select "Disabled", from: "project_project_feature_attributes_merge_requests_access_level"
+ toggle_feature_off('project[project_feature_attributes][merge_requests_access_level]')
save_changes_and_check_activity_tab do
expect(page).to have_content("Comments")
@@ -238,7 +233,7 @@ describe 'Edit Project Settings' do
visit edit_project_path(project)
- select "Disabled", from: "project_project_feature_attributes_repository_access_level"
+ toggle_feature_off('project[project_feature_attributes][repository_access_level]')
save_changes_and_check_activity_tab do
expect(page).not_to have_content("Comments")
@@ -275,4 +270,12 @@ describe 'Edit Project Settings' do
expect(page).not_to have_selector('.project-stats')
end
end
+
+ def toggle_feature_off(feature_name)
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle.checked").click
+ end
+
+ def toggle_feature_on(feature_name)
+ find(".project-feature-controls[data-for=\"#{feature_name}\"] .project-feature-toggle:not(.checked)").click
+ end
end
diff --git a/spec/features/projects/group_links_spec.rb b/spec/features/projects/group_links_spec.rb
deleted file mode 100644
index 5195d027a9f..00000000000
--- a/spec/features/projects/group_links_spec.rb
+++ /dev/null
@@ -1,77 +0,0 @@
-require 'spec_helper'
-
-feature 'Project group links', :js do
- include Select2Helper
-
- let(:master) { create(:user) }
- let(:project) { create(:project) }
- let!(:group) { create(:group) }
-
- background do
- project.add_master(master)
- sign_in(master)
- end
-
- context 'setting an expiration date for a group link' do
- before do
- visit project_settings_members_path(project)
-
- click_on 'share-with-group-tab'
-
- select2 group.id, from: '#link_group_id'
- fill_in 'expires_at_groups', with: (Time.current + 4.5.days).strftime('%Y-%m-%d')
- page.find('body').click
- find('.btn-create').trigger('click')
- end
-
- it 'shows the expiration time with a warning class' do
- page.within('.project-members-groups') do
- expect(page).to have_content('Expires in 4 days')
- expect(page).to have_selector('.text-warning')
- end
- end
- end
-
- context 'nested group project' do
- let!(:nested_group) { create(:group, parent: group) }
- let!(:another_group) { create(:group) }
- let!(:project) { create(:project, namespace: nested_group) }
-
- background do
- group.add_master(master)
- another_group.add_master(master)
- end
-
- it 'does not show ancestors', :nested_groups do
- visit project_settings_members_path(project)
-
- click_on 'share-with-group-tab'
- click_link 'Search for a group'
-
- page.within '.select2-drop' do
- expect(page).to have_content(another_group.name)
- expect(page).not_to have_content(group.name)
- end
- end
- end
-
- describe 'the groups dropdown' do
- before do
- group_two = create(:group)
- group.add_owner(master)
- group_two.add_owner(master)
-
- visit project_settings_members_path(project)
- execute_script 'GroupsSelect.PER_PAGE = 1;'
- open_select2 '#link_group_id'
- end
-
- it 'should infinitely scroll' do
- expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
-
- scroll_select2_to_bottom('.select2-drop .select2-results:visible')
-
- expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
- end
- end
-end
diff --git a/spec/features/projects/import_export/import_file_spec.rb b/spec/features/projects/import_export/import_file_spec.rb
index ad2db1a34f4..e5c7781a096 100644
--- a/spec/features/projects/import_export/import_file_spec.rb
+++ b/spec/features/projects/import_export/import_file_spec.rb
@@ -18,7 +18,7 @@ feature 'Import/Export - project import integration test', js: true do
context 'when selecting the namespace' do
let(:user) { create(:admin) }
- let!(:namespace) { create(:namespace, name: 'asd', owner: user) }
+ let!(:namespace) { user.namespace }
let(:project_path) { 'test-project-path' + SecureRandom.hex }
context 'prefilled the path' do
@@ -66,12 +66,11 @@ feature 'Import/Export - project import integration test', js: true do
end
scenario 'invalid project' do
- namespace = create(:namespace, name: 'asdf', owner: user)
- project = create(:project, namespace: namespace)
+ project = create(:project, namespace: user.namespace)
visit new_project_path
- select2(namespace.id, from: '#project_namespace_id')
+ select2(user.namespace.id, from: '#project_namespace_id')
fill_in :project_path, with: project.name, visible: true
click_link 'GitLab export'
attach_file('file', file)
diff --git a/spec/features/projects/members/group_links_spec.rb b/spec/features/projects/members/groups_with_access_list_spec.rb
index 1c348b987d4..9950272af08 100644
--- a/spec/features/projects/members/group_links_spec.rb
+++ b/spec/features/projects/members/groups_with_access_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-feature 'Projects > Members > Anonymous user sees members', js: true do
+feature 'Projects > Members > Groups with access list', js: true do
let(:user) { create(:user) }
let(:group) { create(:group, :public) }
let(:project) { create(:project, :public) }
@@ -13,7 +13,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
visit project_settings_members_path(project)
end
- it 'updates group access level' do
+ scenario 'updates group access level' do
click_button @group_link.human_access
page.within '.dropdown-menu' do
@@ -27,7 +27,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
expect(first('.group_member')).to have_content('Guest')
end
- it 'updates expiry date' do
+ scenario 'updates expiry date' do
tomorrow = Date.today + 3
fill_in "member_expires_at_#{group.id}", with: tomorrow.strftime("%F")
@@ -38,7 +38,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
end
end
- it 'deletes group link' do
+ scenario 'deletes group link' do
page.within(first('.group_member')) do
find('.btn-remove').click
end
@@ -47,8 +47,8 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
expect(page).not_to have_selector('.group_member')
end
- context 'search' do
- it 'finds no results' do
+ context 'search in existing members (yes, this filters the groups list as well)' do
+ scenario 'finds no results' do
page.within '.member-search-form' do
fill_in 'search', with: 'testing 123'
find('.member-search-btn').click
@@ -57,7 +57,7 @@ feature 'Projects > Members > Anonymous user sees members', js: true do
expect(page).not_to have_selector('.group_member')
end
- it 'finds results' do
+ scenario 'finds results' do
page.within '.member-search-form' do
fill_in 'search', with: group.name
find('.member-search-btn').click
diff --git a/spec/features/projects/members/share_with_group_spec.rb b/spec/features/projects/members/share_with_group_spec.rb
new file mode 100644
index 00000000000..3b368f8e25d
--- /dev/null
+++ b/spec/features/projects/members/share_with_group_spec.rb
@@ -0,0 +1,191 @@
+require 'spec_helper'
+
+feature 'Project > Members > Share with Group', :js do
+ include Select2Helper
+ include ActionView::Helpers::DateHelper
+
+ let(:master) { create(:user) }
+
+ describe 'Share with group lock' do
+ shared_examples 'the project can be shared with groups' do
+ scenario 'the "Share with group" tab exists' do
+ visit project_settings_members_path(project)
+ expect(page).to have_selector('#share-with-group-tab')
+ end
+ end
+
+ shared_examples 'the project cannot be shared with groups' do
+ scenario 'the "Share with group" tab does not exist' do
+ visit project_settings_members_path(project)
+ expect(page).to have_selector('#add-member-tab')
+ expect(page).not_to have_selector('#share-with-group-tab')
+ end
+ end
+
+ context 'for a project in a root group' do
+ let!(:group_to_share_with) { create(:group) }
+ let(:project) { create(:project, namespace: create(:group)) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+ end
+
+ context 'when the group has "Share with group lock" disabled' do
+ it_behaves_like 'the project can be shared with groups'
+
+ scenario 'the project can be shared with another group' do
+ visit project_settings_members_path(project)
+
+ click_on 'share-with-group-tab'
+
+ select2 group_to_share_with.id, from: '#link_group_id'
+ page.find('body').click
+ find('.btn-create').trigger('click')
+
+ page.within('.project-members-groups') do
+ expect(page).to have_content(group_to_share_with.name)
+ end
+ end
+ end
+
+ context 'when the group has "Share with group lock" enabled' do
+ before do
+ project.namespace.update_column(:share_with_group_lock, true)
+ end
+
+ it_behaves_like 'the project cannot be shared with groups'
+ end
+ end
+
+ context 'for a project in a subgroup', :nested_groups do
+ let!(:group_to_share_with) { create(:group) }
+ let(:root_group) { create(:group) }
+ let(:subgroup) { create(:group, parent: root_group) }
+ let(:project) { create(:project, namespace: subgroup) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+ end
+
+ context 'when the root_group has "Share with group lock" disabled' do
+ context 'when the subgroup has "Share with group lock" disabled' do
+ it_behaves_like 'the project can be shared with groups'
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ it_behaves_like 'the project cannot be shared with groups'
+ end
+ end
+
+ context 'when the root_group has "Share with group lock" enabled' do
+ before do
+ root_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
+ it_behaves_like 'the project can be shared with groups'
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ it_behaves_like 'the project cannot be shared with groups'
+ end
+ end
+ end
+ end
+
+ describe 'setting an expiration date for a group link' do
+ let(:project) { create(:project) }
+ let!(:group) { create(:group) }
+
+ around do |example|
+ Timecop.freeze { example.run }
+ end
+
+ before do
+ project.add_master(master)
+ sign_in(master)
+
+ visit project_settings_members_path(project)
+
+ click_on 'share-with-group-tab'
+
+ select2 group.id, from: '#link_group_id'
+
+ fill_in 'expires_at_groups', with: (Time.now + 4.5.days).strftime('%Y-%m-%d')
+ page.find('body').click
+ find('.btn-create').trigger('click')
+ end
+
+ scenario 'the group link shows the expiration time with a warning class' do
+ page.within('.project-members-groups') do
+ # Using distance_of_time_in_words_to_now because it is not the same as
+ # subtraction, and this way avoids time zone issues as well
+ expires_in_text = distance_of_time_in_words_to_now(project.project_group_links.first.expires_at)
+ expect(page).to have_content(expires_in_text)
+ expect(page).to have_selector('.text-warning')
+ end
+ end
+ end
+
+ describe 'the groups dropdown' do
+ context 'with multiple groups to choose from' do
+ let(:project) { create(:project) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+
+ create(:group).add_owner(master)
+ create(:group).add_owner(master)
+
+ visit project_settings_members_path(project)
+ execute_script 'GroupsSelect.PER_PAGE = 1;'
+ open_select2 '#link_group_id'
+ end
+
+ it 'should infinitely scroll' do
+ expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 1)
+
+ scroll_select2_to_bottom('.select2-drop .select2-results:visible')
+
+ expect(find('.select2-drop .select2-results')).to have_selector('.select2-result', count: 2)
+ end
+ end
+
+ context 'for a project in a nested group' do
+ let(:group) { create(:group) }
+ let!(:nested_group) { create(:group, parent: group) }
+ let!(:group_to_share_with) { create(:group) }
+ let!(:project) { create(:project, namespace: nested_group) }
+
+ background do
+ project.add_master(master)
+ sign_in(master)
+ group.add_master(master)
+ group_to_share_with.add_master(master)
+ end
+
+ scenario 'the groups dropdown does not show ancestors', :nested_groups do
+ visit project_settings_members_path(project)
+
+ click_on 'share-with-group-tab'
+ click_link 'Search for a group'
+
+ page.within '.select2-drop' do
+ expect(page).to have_content(group_to_share_with.name)
+ expect(page).not_to have_content(group.name)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/settings/merge_requests_settings_spec.rb b/spec/features/projects/settings/merge_requests_settings_spec.rb
index 104ce08d9f3..b1ec556bf16 100644
--- a/spec/features/projects/settings/merge_requests_settings_spec.rb
+++ b/spec/features/projects/settings/merge_requests_settings_spec.rb
@@ -19,8 +19,8 @@ feature 'Project settings > Merge Requests', :js do
expect(page).to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
- select 'Disabled', from: "project_project_feature_attributes_merge_requests_access_level"
within('.sharing-permissions-form') do
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
@@ -39,8 +39,8 @@ feature 'Project settings > Merge Requests', :js do
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).to have_content('Only allow merge requests to be merged if all discussions are resolved')
- select 'Everyone with access', from: "project_project_feature_attributes_builds_access_level"
within('.sharing-permissions-form') do
+ find('.project-feature-controls[data-for="project[project_feature_attributes][builds_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
@@ -60,8 +60,8 @@ feature 'Project settings > Merge Requests', :js do
expect(page).not_to have_content('Only allow merge requests to be merged if the pipeline succeeds')
expect(page).not_to have_content('Only allow merge requests to be merged if all discussions are resolved')
- select 'Everyone with access', from: "project_project_feature_attributes_merge_requests_access_level"
within('.sharing-permissions-form') do
+ find('.project-feature-controls[data-for="project[project_feature_attributes][merge_requests_access_level]"] .project-feature-toggle').click
click_on('Save changes')
end
diff --git a/spec/features/projects/settings/pipelines_settings_spec.rb b/spec/features/projects/settings/pipelines_settings_spec.rb
index 232d796a200..975d204e75e 100644
--- a/spec/features/projects/settings/pipelines_settings_spec.rb
+++ b/spec/features/projects/settings/pipelines_settings_spec.rb
@@ -41,5 +41,15 @@ feature "Pipelines settings" do
checkbox = find_field('project_auto_cancel_pending_pipelines')
expect(checkbox).to be_checked
end
+
+ scenario 'update auto devops settings' do
+ fill_in('project_auto_devops_attributes_domain', with: 'test.com')
+ page.choose('project_auto_devops_attributes_enabled_false')
+ click_on 'Save changes'
+
+ expect(page.status_code).to eq(200)
+ expect(project.auto_devops).to be_present
+ expect(project.auto_devops).not_to be_enabled
+ end
end
end
diff --git a/spec/features/projects/settings/visibility_settings_spec.rb b/spec/features/projects/settings/visibility_settings_spec.rb
index 1756c7d00fe..37ee6255bd1 100644
--- a/spec/features/projects/settings/visibility_settings_spec.rb
+++ b/spec/features/projects/settings/visibility_settings_spec.rb
@@ -11,19 +11,19 @@ feature 'Visibility settings', js: true do
end
scenario 'project visibility select is available' do
- visibility_select_container = find('.js-visibility-select')
+ visibility_select_container = find('.project-visibility-setting')
- expect(visibility_select_container.find('.visibility-select').value).to eq project.visibility_level.to_s
- expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
+ expect(visibility_select_container.find('select').value).to eq project.visibility_level.to_s
+ expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.'
end
scenario 'project visibility description updates on change' do
- visibility_select_container = find('.js-visibility-select')
- visibility_select = visibility_select_container.find('.visibility-select')
+ visibility_select_container = find('.project-visibility-setting')
+ visibility_select = visibility_select_container.find('select')
visibility_select.select('Private')
expect(visibility_select.value).to eq '0'
- expect(visibility_select_container).to have_content 'Project access must be granted explicitly to each user.'
+ expect(visibility_select_container).to have_content 'Access must be granted explicitly to each user.'
end
end
@@ -37,11 +37,10 @@ feature 'Visibility settings', js: true do
end
scenario 'project visibility is locked' do
- visibility_select_container = find('.js-visibility-select')
+ visibility_select_container = find('.project-visibility-setting')
- expect(visibility_select_container).not_to have_select '.visibility-select'
- expect(visibility_select_container).to have_content 'Public'
- expect(visibility_select_container).to have_content 'The project can be accessed without any authentication.'
+ expect(visibility_select_container).to have_selector 'select[name="project[visibility_level]"]:disabled'
+ expect(visibility_select_container).to have_content 'The project can be accessed by anyone, regardless of authentication.'
end
end
end
diff --git a/spec/finders/group_members_finder_spec.rb b/spec/finders/group_members_finder_spec.rb
index db3fcc23475..9f285e28535 100644
--- a/spec/finders/group_members_finder_spec.rb
+++ b/spec/finders/group_members_finder_spec.rb
@@ -15,7 +15,7 @@ describe GroupMembersFinder, '#execute' do
result = described_class.new(group).execute
- expect(result.to_a).to eq([member3, member2, member1])
+ expect(result.to_a).to match_array([member3, member2, member1])
end
it 'returns members for nested group', :nested_groups do
@@ -27,6 +27,6 @@ describe GroupMembersFinder, '#execute' do
result = described_class.new(nested_group).execute
- expect(result.to_a).to eq([member4, member3, member1])
+ expect(result.to_a).to match_array([member1, member3, member4])
end
end
diff --git a/spec/finders/members_finder_spec.rb b/spec/finders/members_finder_spec.rb
index 300ba8422e8..7bb1f45322e 100644
--- a/spec/finders/members_finder_spec.rb
+++ b/spec/finders/members_finder_spec.rb
@@ -17,6 +17,6 @@ describe MembersFinder, '#execute' do
result = described_class.new(project, user2).execute
- expect(result.to_a).to eq([member3, member2, member1])
+ expect(result.to_a).to match_array([member1, member2, member3])
end
end
diff --git a/spec/fixtures/api/schemas/public_api/v4/user/login.json b/spec/fixtures/api/schemas/public_api/v4/user/login.json
index 6181b3ccc86..e6c1d9c9d84 100644
--- a/spec/fixtures/api/schemas/public_api/v4/user/login.json
+++ b/spec/fixtures/api/schemas/public_api/v4/user/login.json
@@ -19,6 +19,7 @@
"organization",
"last_sign_in_at",
"confirmed_at",
+ "theme_id",
"color_scheme_id",
"projects_limit",
"current_sign_in_at",
diff --git a/spec/helpers/auto_devops_helper_spec.rb b/spec/helpers/auto_devops_helper_spec.rb
new file mode 100644
index 00000000000..b6d892548ef
--- /dev/null
+++ b/spec/helpers/auto_devops_helper_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe AutoDevopsHelper do
+ set(:project) { create(:project) }
+ set(:user) { create(:user) }
+
+ describe '.show_auto_devops_callout?' do
+ let(:allowed) { true }
+
+ before do
+ allow(helper).to receive(:can?).with(user, :admin_pipeline, project) { allowed }
+ allow(helper).to receive(:current_user) { user }
+ end
+
+ subject { helper.show_auto_devops_callout?(project) }
+
+ context 'when all conditions are met' do
+ it { is_expected.to eq(true) }
+ end
+
+ context 'when dismissed' do
+ before do
+ helper.request.cookies[:auto_devops_settings_dismissed] = 'true'
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when user cannot admin project' do
+ let(:allowed) { false }
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is enabled system-wide' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly enabled for project' do
+ before do
+ project.create_auto_devops!(enabled: true)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+
+ context 'when auto devops is explicitly disabled for project' do
+ before do
+ project.create_auto_devops!(enabled: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+end
diff --git a/spec/helpers/commits_helper_spec.rb b/spec/helpers/commits_helper_spec.rb
index 7179185285c..4b6c7c33e5b 100644
--- a/spec/helpers/commits_helper_spec.rb
+++ b/spec/helpers/commits_helper_spec.rb
@@ -12,6 +12,17 @@ describe CommitsHelper do
expect(helper.commit_author_link(commit))
.not_to include('onmouseover="alert(1)"')
end
+
+ it 'escapes the author name' do
+ user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>')
+
+ commit = double(author: user, author_name: '', author_email: '')
+
+ expect(helper.commit_author_link(commit))
+ .to include('Foo &lt;script&gt;')
+ expect(helper.commit_author_link(commit, avatar: true))
+ .to include('commit-author-name', 'Foo &lt;script&gt;')
+ end
end
describe 'commit_committer_link' do
@@ -25,6 +36,17 @@ describe CommitsHelper do
expect(helper.commit_committer_link(commit))
.not_to include('onmouseover="alert(1)"')
end
+
+ it 'escapes the commiter name' do
+ user = build_stubbed(:user, name: 'Foo <script>alert("XSS")</script>')
+
+ commit = double(committer: user, committer_name: '', committer_email: '')
+
+ expect(helper.commit_committer_link(commit))
+ .to include('Foo &lt;script&gt;')
+ expect(helper.commit_committer_link(commit, avatar: true))
+ .to include('commit-committer-name', 'Foo &lt;script&gt;')
+ end
end
describe '#view_on_environment_button' do
diff --git a/spec/helpers/groups_helper_spec.rb b/spec/helpers/groups_helper_spec.rb
index 05f969904f5..36031ac1a28 100644
--- a/spec/helpers/groups_helper_spec.rb
+++ b/spec/helpers/groups_helper_spec.rb
@@ -95,4 +95,97 @@ describe GroupsHelper do
.to match(/<li style="text-indent: 16px;"><a.*>#{deep_nested_group.name}.*<\/li>.*<a.*>#{very_deep_nested_group.name}<\/a>/m)
end
end
+
+ # rubocop:disable Layout/SpaceBeforeComma
+ describe '#share_with_group_lock_help_text', :nested_groups do
+ let!(:root_group) { create(:group) }
+ let!(:subgroup) { create(:group, parent: root_group) }
+ let!(:sub_subgroup) { create(:group, parent: subgroup) }
+ let(:root_owner) { create(:user) }
+ let(:sub_owner) { create(:user) }
+ let(:sub_sub_owner) { create(:user) }
+ let(:possible_help_texts) do
+ {
+ default_help: "This setting will be applied to all subgroups unless overridden by a group owner",
+ ancestor_locked_but_you_can_override: /This setting is applied on <a .+>.+<\/a>\. You can override the setting or .+/,
+ ancestor_locked_so_ask_the_owner: /This setting is applied on .+\. To share projects in this group with another group, ask the owner to override the setting or remove the share with group lock from .+/,
+ ancestor_locked_and_has_been_overridden: /This setting is applied on .+ and has been overridden on this subgroup/
+ }
+ end
+ let(:possible_linked_ancestors) do
+ {
+ root_group: root_group,
+ subgroup: subgroup
+ }
+ end
+ let(:users) do
+ {
+ root_owner: root_owner,
+ sub_owner: sub_owner,
+ sub_sub_owner: sub_sub_owner
+ }
+ end
+ subject { helper.share_with_group_lock_help_text(sub_subgroup) }
+
+ where(:root_share_with_group_locked, :subgroup_share_with_group_locked, :sub_subgroup_share_with_group_locked, :current_user, :help_text, :linked_ancestor) do
+ [
+ [false , false , false , :root_owner , :default_help , nil],
+ [false , false , false , :sub_owner , :default_help , nil],
+ [false , false , false , :sub_sub_owner , :default_help , nil],
+ [false , false , true , :root_owner , :default_help , nil],
+ [false , false , true , :sub_owner , :default_help , nil],
+ [false , false , true , :sub_sub_owner , :default_help , nil],
+ [false , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
+ [false , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
+ [false , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :subgroup],
+ [false , true , true , :root_owner , :ancestor_locked_but_you_can_override , :subgroup],
+ [false , true , true , :sub_owner , :ancestor_locked_but_you_can_override , :subgroup],
+ [false , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :subgroup],
+ [true , false , false , :root_owner , :default_help , nil],
+ [true , false , false , :sub_owner , :default_help , nil],
+ [true , false , false , :sub_sub_owner , :default_help , nil],
+ [true , false , true , :root_owner , :default_help , nil],
+ [true , false , true , :sub_owner , :default_help , nil],
+ [true , false , true , :sub_sub_owner , :default_help , nil],
+ [true , true , false , :root_owner , :ancestor_locked_and_has_been_overridden , :root_group],
+ [true , true , false , :sub_owner , :ancestor_locked_and_has_been_overridden , :root_group],
+ [true , true , false , :sub_sub_owner , :ancestor_locked_and_has_been_overridden , :root_group],
+ [true , true , true , :root_owner , :ancestor_locked_but_you_can_override , :root_group],
+ [true , true , true , :sub_owner , :ancestor_locked_so_ask_the_owner , :root_group],
+ [true , true , true , :sub_sub_owner , :ancestor_locked_so_ask_the_owner , :root_group]
+ ]
+ end
+
+ with_them do
+ before do
+ root_group.add_owner(root_owner)
+ subgroup.add_owner(sub_owner)
+ sub_subgroup.add_owner(sub_sub_owner)
+
+ root_group.update_column(:share_with_group_lock, true) if root_share_with_group_locked
+ subgroup.update_column(:share_with_group_lock, true) if subgroup_share_with_group_locked
+ sub_subgroup.update_column(:share_with_group_lock, true) if sub_subgroup_share_with_group_locked
+
+ allow(helper).to receive(:current_user).and_return(users[current_user])
+ allow(helper).to receive(:can?)
+ .with(users[current_user], :change_share_with_group_lock, subgroup)
+ .and_return(Ability.allowed?(users[current_user], :change_share_with_group_lock, subgroup))
+
+ ancestor = possible_linked_ancestors[linked_ancestor]
+ if ancestor
+ allow(helper).to receive(:can?)
+ .with(users[current_user], :read_group, ancestor)
+ .and_return(Ability.allowed?(users[current_user], :read_group, ancestor))
+ allow(helper).to receive(:can?)
+ .with(users[current_user], :admin_group, ancestor)
+ .and_return(Ability.allowed?(users[current_user], :admin_group, ancestor))
+ end
+ end
+
+ it 'has the correct help text with correct ancestor links' do
+ expect(subject).to match(possible_help_texts[help_text])
+ expect(subject).to match(possible_linked_ancestors[linked_ancestor].name) unless help_text == :default_help
+ end
+ end
+ end
end
diff --git a/spec/helpers/preferences_helper_spec.rb b/spec/helpers/preferences_helper_spec.rb
index a04c87b08eb..8b8080563d3 100644
--- a/spec/helpers/preferences_helper_spec.rb
+++ b/spec/helpers/preferences_helper_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe PreferencesHelper do
- describe 'dashboard_choices' do
+ describe '#dashboard_choices' do
it 'raises an exception when defined choices may be missing' do
expect(User).to receive(:dashboards).and_return(foo: 'foo')
expect { helper.dashboard_choices }.to raise_error(RuntimeError)
@@ -26,7 +26,33 @@ describe PreferencesHelper do
end
end
- describe 'user_color_scheme' do
+ describe '#user_application_theme' do
+ context 'with a user' do
+ it "returns user's theme's css_class" do
+ stub_user(theme_id: 3)
+
+ expect(helper.user_application_theme).to eq 'ui_light'
+ end
+
+ it 'returns the default when id is invalid' do
+ stub_user(theme_id: Gitlab::Themes.count + 5)
+
+ allow(Gitlab.config.gitlab).to receive(:default_theme).and_return(1)
+
+ expect(helper.user_application_theme).to eq 'ui_indigo'
+ end
+ end
+
+ context 'without a user' do
+ it 'returns the default theme' do
+ stub_user
+
+ expect(helper.user_application_theme).to eq Gitlab::Themes.default.css_class
+ end
+ end
+ end
+
+ describe '#user_color_scheme' do
context 'with a user' do
it "returns user's scheme's css_class" do
allow(helper).to receive(:current_user)
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index d1efa318d14..49cb7c954b4 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -191,10 +191,31 @@ describe ProjectsHelper do
end
end
- describe 'link_to_member' do
- let(:group) { create(:group) }
- let(:project) { create(:project, group: group) }
- let(:user) { create(:user) }
+ describe '#link_to_member_avatar' do
+ let(:user) { build_stubbed(:user) }
+ let(:expected) { double }
+
+ before do
+ expect(helper).to receive(:avatar_icon).with(user, 16).and_return(expected)
+ end
+
+ it 'returns image tag for member avatar' do
+ expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16"], alt: "" })
+
+ helper.link_to_member_avatar(user)
+ end
+
+ it 'returns image tag with avatar class' do
+ expect(helper).to receive(:image_tag).with(expected, { width: 16, class: ["avatar", "avatar-inline", "s16", "any-avatar-class"], alt: "" })
+
+ helper.link_to_member_avatar(user, avatar_class: "any-avatar-class")
+ end
+ end
+
+ describe '#link_to_member' do
+ let(:group) { build_stubbed(:group) }
+ let(:project) { build_stubbed(:project, group: group) }
+ let(:user) { build_stubbed(:user) }
describe 'using the default options' do
it 'returns an HTML link to the user' do
diff --git a/spec/javascripts/commit/pipelines/pipelines_spec.js b/spec/javascripts/commit/pipelines/pipelines_spec.js
index a34cadec0ab..454f187ccbc 100644
--- a/spec/javascripts/commit/pipelines/pipelines_spec.js
+++ b/spec/javascripts/commit/pipelines/pipelines_spec.js
@@ -29,6 +29,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
@@ -64,6 +65,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
@@ -115,6 +117,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
element.appendChild(this.component.$el);
@@ -136,6 +139,7 @@ describe('Pipelines table in Commits and Merge requests', () => {
propsData: {
endpoint: 'endpoint',
helpPagePath: 'foo',
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
diff --git a/spec/javascripts/monitoring/graph/legend_spec.js b/spec/javascripts/monitoring/graph/legend_spec.js
index da2fbd26e23..2571b7ef869 100644
--- a/spec/javascripts/monitoring/graph/legend_spec.js
+++ b/spec/javascripts/monitoring/graph/legend_spec.js
@@ -28,7 +28,7 @@ const defaultValuesComponent = {
currentDataIndex: 0,
};
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result,
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0],
defaultValuesComponent.graphWidth, defaultValuesComponent.graphHeight,
defaultValuesComponent.graphHeightOffset);
@@ -89,13 +89,12 @@ describe('GraphLegend', () => {
expect(component.$el.querySelectorAll('.rect-axis-text').length).toEqual(2);
});
- it('contains text to signal the usage, title and time', () => {
+ it('contains text to signal the usage, title and time with multiple time series', () => {
const component = createComponent(defaultValuesComponent);
const titles = component.$el.querySelectorAll('.legend-metric-title');
- expect(getTextFromNode(component, '.legend-metric-title').indexOf(component.legendTitle)).not.toEqual(-1);
- expect(titles[0].textContent.indexOf('Title')).not.toEqual(-1);
- expect(titles[1].textContent.indexOf('Series')).not.toEqual(-1);
+ expect(titles[0].textContent.indexOf('1xx')).not.toEqual(-1);
+ expect(titles[1].textContent.indexOf('2xx')).not.toEqual(-1);
expect(getTextFromNode(component, '.y-label-text')).toEqual(component.yAxisLabel);
});
diff --git a/spec/javascripts/monitoring/monitoring_paths_spec.js b/spec/javascripts/monitoring/graph_path_spec.js
index d39db945e17..a4844636d09 100644
--- a/spec/javascripts/monitoring/monitoring_paths_spec.js
+++ b/spec/javascripts/monitoring/graph_path_spec.js
@@ -1,10 +1,10 @@
import Vue from 'vue';
-import MonitoringPaths from '~/monitoring/components/monitoring_paths.vue';
+import GraphPath from '~/monitoring/components/graph_path.vue';
import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { singleRowMetricsMultipleSeries, convertDatesMultipleSeries } from './mock_data';
const createComponent = (propsData) => {
- const Component = Vue.extend(MonitoringPaths);
+ const Component = Vue.extend(GraphPath);
return new Component({
propsData,
@@ -13,22 +13,23 @@ const createComponent = (propsData) => {
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const firstTimeSeries = timeSeries[0];
describe('Monitoring Paths', () => {
it('renders two paths to represent a line and the area underneath it', () => {
const component = createComponent({
- generatedLinePath: timeSeries[0].linePath,
- generatedAreaPath: timeSeries[0].areaPath,
- lineColor: '#ccc',
- areaColor: '#fff',
+ generatedLinePath: firstTimeSeries.linePath,
+ generatedAreaPath: firstTimeSeries.areaPath,
+ lineColor: firstTimeSeries.lineColor,
+ areaColor: firstTimeSeries.areaColor,
});
const metricArea = component.$el.querySelector('.metric-area');
const metricLine = component.$el.querySelector('.metric-line');
- expect(metricArea.getAttribute('fill')).toBe('#fff');
- expect(metricArea.getAttribute('d')).toBe(timeSeries[0].areaPath);
- expect(metricLine.getAttribute('stroke')).toBe('#ccc');
- expect(metricLine.getAttribute('d')).toBe(timeSeries[0].linePath);
+ expect(metricArea.getAttribute('fill')).toBe('#8fbce8');
+ expect(metricArea.getAttribute('d')).toBe(firstTimeSeries.areaPath);
+ expect(metricLine.getAttribute('stroke')).toBe('#1f78d1');
+ expect(metricLine.getAttribute('d')).toBe(firstTimeSeries.linePath);
});
});
diff --git a/spec/javascripts/monitoring/mock_data.js b/spec/javascripts/monitoring/mock_data.js
index 3d399f2bb95..7ceab657464 100644
--- a/spec/javascripts/monitoring/mock_data.js
+++ b/spec/javascripts/monitoring/mock_data.js
@@ -6346,7 +6346,13 @@ export const singleRowMetricsMultipleSeries = [
}
]
},
- ]
+ ],
+ 'when': [
+ {
+ 'value': 'hundred(s)',
+ 'color': 'green',
+ },
+ ],
}
]
},
diff --git a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
index 3daf6bf82df..7e44a9ade9e 100644
--- a/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
+++ b/spec/javascripts/monitoring/utils/multiple_time_series_spec.js
@@ -2,16 +2,17 @@ import createTimeSeries from '~/monitoring/utils/multiple_time_series';
import { convertDatesMultipleSeries, singleRowMetricsMultipleSeries } from '../mock_data';
const convertedMetrics = convertDatesMultipleSeries(singleRowMetricsMultipleSeries);
-const timeSeries = createTimeSeries(convertedMetrics[0].queries[0].result, 428, 272, 120);
+const timeSeries = createTimeSeries(convertedMetrics[0].queries[0], 428, 272, 120);
+const firstTimeSeries = timeSeries[0];
describe('Multiple time series', () => {
it('createTimeSeries returned array contains an object for each element', () => {
- expect(typeof timeSeries[0].linePath).toEqual('string');
- expect(typeof timeSeries[0].areaPath).toEqual('string');
- expect(typeof timeSeries[0].timeSeriesScaleX).toEqual('function');
- expect(typeof timeSeries[0].areaColor).toEqual('string');
- expect(typeof timeSeries[0].lineColor).toEqual('string');
- expect(timeSeries[0].values instanceof Array).toEqual(true);
+ expect(typeof firstTimeSeries.linePath).toEqual('string');
+ expect(typeof firstTimeSeries.areaPath).toEqual('string');
+ expect(typeof firstTimeSeries.timeSeriesScaleX).toEqual('function');
+ expect(typeof firstTimeSeries.areaColor).toEqual('string');
+ expect(typeof firstTimeSeries.lineColor).toEqual('string');
+ expect(firstTimeSeries.values instanceof Array).toEqual(true);
});
it('createTimeSeries returns an array', () => {
diff --git a/spec/javascripts/notes_spec.js b/spec/javascripts/notes_spec.js
index 8c5ad8914b0..3e791a31604 100644
--- a/spec/javascripts/notes_spec.js
+++ b/spec/javascripts/notes_spec.js
@@ -770,6 +770,20 @@ import '~/notes';
expect($tempNote.prop('nodeName')).toEqual('LI');
expect($tempNote.find('.timeline-content').hasClass('discussion')).toBeTruthy();
});
+
+ it('should return a escaped user name', () => {
+ const currentUserFullnameXSS = 'Foo <script>alert("XSS")</script>';
+ const $tempNote = this.notes.createPlaceholderNote({
+ formContent: sampleComment,
+ uniqueId,
+ isDiscussionNote: false,
+ currentUsername,
+ currentUserFullname: currentUserFullnameXSS,
+ currentUserAvatar,
+ });
+ const $tempNoteHeader = $tempNote.find('.note-header');
+ expect($tempNoteHeader.find('.hidden-xs').text().trim()).toEqual('Foo &lt;script&gt;alert(&quot;XSS&quot;)&lt;/script&gt;');
+ });
});
describe('createPlaceholderSystemNote', () => {
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index 3c4b20a5f06..256fdbe743c 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -16,6 +16,7 @@ describe('Pipeline Url Component', () => {
path: 'foo',
flags: {},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -30,6 +31,7 @@ describe('Pipeline Url Component', () => {
path: 'foo',
flags: {},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -50,6 +52,7 @@ describe('Pipeline Url Component', () => {
path: '/',
},
},
+ autoDevopsHelpPath: 'foo',
};
const component = new PipelineUrlComponent({
@@ -73,6 +76,7 @@ describe('Pipeline Url Component', () => {
path: 'foo',
flags: {},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -91,6 +95,7 @@ describe('Pipeline Url Component', () => {
stuck: true,
},
},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
@@ -98,4 +103,26 @@ describe('Pipeline Url Component', () => {
expect(component.$el.querySelector('.js-pipeline-url-yaml').textContent).toContain('yaml invalid');
expect(component.$el.querySelector('.js-pipeline-url-stuck').textContent).toContain('stuck');
});
+
+ it('should render a badge for autodevops', () => {
+ const component = new PipelineUrlComponent({
+ propsData: {
+ pipeline: {
+ id: 1,
+ path: 'foo',
+ flags: {
+ latest: true,
+ yaml_errors: true,
+ stuck: true,
+ auto_devops: true,
+ },
+ },
+ autoDevopsHelpPath: 'foo',
+ },
+ }).$mount();
+
+ expect(
+ component.$el.querySelector('.js-pipeline-url-autodevops').textContent.trim(),
+ ).toEqual('Auto DevOps');
+ });
});
diff --git a/spec/javascripts/pipelines/pipelines_table_row_spec.js b/spec/javascripts/pipelines/pipelines_table_row_spec.js
index 7ce39dca112..d7456a48bc1 100644
--- a/spec/javascripts/pipelines/pipelines_table_row_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_row_spec.js
@@ -9,7 +9,7 @@ describe('Pipelines Table Row', () => {
el: document.querySelector('.test-dom-element'),
propsData: {
pipeline,
- service: {},
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
};
diff --git a/spec/javascripts/pipelines/pipelines_table_spec.js b/spec/javascripts/pipelines/pipelines_table_spec.js
index 3afe89c8db4..4f5eb42eb35 100644
--- a/spec/javascripts/pipelines/pipelines_table_spec.js
+++ b/spec/javascripts/pipelines/pipelines_table_spec.js
@@ -22,6 +22,7 @@ describe('Pipelines Table', () => {
component = new PipelinesTableComponent({
propsData: {
pipelines: [],
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
});
@@ -47,6 +48,7 @@ describe('Pipelines Table', () => {
const component = new PipelinesTableComponent({
propsData: {
pipelines: [],
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
expect(component.$el.querySelectorAll('.commit.gl-responsive-table-row').length).toEqual(0);
@@ -58,6 +60,7 @@ describe('Pipelines Table', () => {
const component = new PipelinesTableComponent({
propsData: {
pipelines: [pipeline],
+ autoDevopsHelpPath: 'foo',
},
}).$mount();
diff --git a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
index 59fc2dedba5..67f8a8946c2 100644
--- a/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
+++ b/spec/javascripts/projects_dropdown/components/projects_list_search_spec.js
@@ -43,7 +43,7 @@ describe('ProjectsListSearchComponent', () => {
expect(vm.listEmptyMessage).toBe('Something went wrong on our end.');
vm.searchFailed = false;
- expect(vm.listEmptyMessage).toBe('No projects matched your query');
+ expect(vm.listEmptyMessage).toBe('Sorry, no projects matched your search');
});
});
});
diff --git a/spec/javascripts/projects_dropdown/components/search_spec.js b/spec/javascripts/projects_dropdown/components/search_spec.js
index f2a23e33325..24d8a00b254 100644
--- a/spec/javascripts/projects_dropdown/components/search_spec.js
+++ b/spec/javascripts/projects_dropdown/components/search_spec.js
@@ -94,7 +94,7 @@ describe('SearchComponent', () => {
expect(vm.$el.classList.contains('search-input-container')).toBeTruthy();
expect(vm.$el.classList.contains('hidden-xs')).toBeTruthy();
expect(inputEl).not.toBe(null);
- expect(inputEl.getAttribute('placeholder')).toBe('Search projects');
+ expect(inputEl.getAttribute('placeholder')).toBe('Search your projects');
expect(vm.$el.querySelector('.search-icon')).toBeDefined();
});
});
diff --git a/spec/javascripts/user_callout_spec.js b/spec/javascripts/user_callout_spec.js
index 28d0c7dcd99..69cb93bd850 100644
--- a/spec/javascripts/user_callout_spec.js
+++ b/spec/javascripts/user_callout_spec.js
@@ -33,4 +33,17 @@ describe('UserCallout', function () {
this.userCalloutBtn.click();
expect(Cookies.get(USER_CALLOUT_COOKIE)).toBe('true');
});
+
+ describe('Sets cookie with setCalloutPerProject', () => {
+ beforeEach(() => {
+ spyOn(Cookies, 'set').and.callFake(() => {});
+ document.querySelector('.user-callout').setAttribute('data-project-path', 'foo/bar');
+ this.userCallout = new UserCallout({ setCalloutPerProject: true });
+ });
+
+ it('sets a cookie when the user clicks the close button', () => {
+ this.userCalloutBtn.click();
+ expect(Cookies.set).toHaveBeenCalledWith('user_callout_dismissed', 'true', Object({ expires: 365, path: 'foo/bar' }));
+ });
+ });
});
diff --git a/spec/lib/banzai/filter/sanitization_filter_spec.rb b/spec/lib/banzai/filter/sanitization_filter_spec.rb
index 35a32a46eff..01ceb21dfaa 100644
--- a/spec/lib/banzai/filter/sanitization_filter_spec.rb
+++ b/spec/lib/banzai/filter/sanitization_filter_spec.rb
@@ -49,7 +49,7 @@ describe Banzai::Filter::SanitizationFilter do
instance = described_class.new('Foo')
3.times { instance.whitelist }
- expect(instance.whitelist[:transformers].size).to eq 4
+ expect(instance.whitelist[:transformers].size).to eq 5
end
it 'sanitizes `class` attribute from all elements' do
@@ -63,8 +63,8 @@ describe Banzai::Filter::SanitizationFilter do
expect(filter(act).to_html).to eq %q{<span>def</span>}
end
- it 'allows `style` attribute on table elements' do
- html = <<-HTML.strip_heredoc
+ it 'allows `text-align` property in `style` attribute on table elements' do
+ html = <<~HTML
<table>
<tr><th style="text-align: center">Head</th></tr>
<tr><td style="text-align: right">Body</th></tr>
@@ -77,6 +77,20 @@ describe Banzai::Filter::SanitizationFilter do
expect(doc.at_css('td')['style']).to eq 'text-align: right'
end
+ it 'disallows other properties in `style` attribute on table elements' do
+ html = <<~HTML
+ <table>
+ <tr><th style="text-align: foo">Head</th></tr>
+ <tr><td style="position: fixed; height: 50px; width: 50px; background: red; z-index: 999; font-size: 36px; text-align: center">Body</th></tr>
+ </table>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('th')['style']).to be_nil
+ expect(doc.at_css('td')['style']).to eq 'text-align: center'
+ end
+
it 'allows `span` elements' do
exp = act = %q{<span>Hello</span>}
expect(filter(act).to_html).to eq exp
@@ -87,6 +101,18 @@ describe Banzai::Filter::SanitizationFilter do
expect(filter(act).to_html).to eq exp
end
+ it 'disallows the `name` attribute globally' do
+ html = <<~HTML
+ <img name="getElementById" src="">
+ <span name="foo" class="bar">Hi</span>
+ HTML
+
+ doc = filter(html)
+
+ expect(doc.at_css('img')).not_to have_attribute('name')
+ expect(doc.at_css('span')).not_to have_attribute('name')
+ end
+
it 'allows `summary` elements' do
exp = act = '<summary>summary line</summary>'
expect(filter(act).to_html).to eq exp
diff --git a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
index b155c20d8d3..cb52d971047 100644
--- a/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
+++ b/spec/lib/gitlab/background_migration/migrate_events_to_push_event_payloads_spec.rb
@@ -215,9 +215,17 @@ end
# to a specific version of the database where said table is still present.
#
describe Gitlab::BackgroundMigration::MigrateEventsToPushEventPayloads, :migration, schema: 20170825154015 do
+ let(:user_class) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'users'
+ end
+ end
+
let(:migration) { described_class.new }
- let(:project) { create(:project_empty_repo) }
- let(:author) { create(:user) }
+ let(:user_class) { table(:users) }
+ let(:author) { build(:user).becomes(user_class).tap(&:save!).becomes(User) }
+ let(:namespace) { create(:namespace, owner: author) }
+ let(:project) { create(:project_empty_repo, namespace: namespace, creator: author) }
# We can not rely on FactoryGirl as the state of Event may change in ways that
# the background migration does not expect, hence we use the Event class of
diff --git a/spec/lib/gitlab/exclusive_lease_spec.rb b/spec/lib/gitlab/exclusive_lease_spec.rb
index c1ed47cf64a..7322a326b01 100644
--- a/spec/lib/gitlab/exclusive_lease_spec.rb
+++ b/spec/lib/gitlab/exclusive_lease_spec.rb
@@ -47,6 +47,18 @@ describe Gitlab::ExclusiveLease, :clean_gitlab_redis_shared_state do
end
end
+ describe '.get_uuid' do
+ it 'gets the uuid if lease with the key associated exists' do
+ uuid = described_class.new(unique_key, timeout: 3600).try_obtain
+
+ expect(described_class.get_uuid(unique_key)).to eq(uuid)
+ end
+
+ it 'returns false if the lease does not exist' do
+ expect(described_class.get_uuid(unique_key)).to be false
+ end
+ end
+
describe '.cancel' do
it 'can cancel a lease' do
uuid = new_lease(unique_key)
diff --git a/spec/lib/gitlab/gitaly_client_spec.rb b/spec/lib/gitlab/gitaly_client_spec.rb
index 921e786a55c..a9b861fcff2 100644
--- a/spec/lib/gitlab/gitaly_client_spec.rb
+++ b/spec/lib/gitlab/gitaly_client_spec.rb
@@ -102,6 +102,22 @@ describe Gitlab::GitalyClient, skip_gitaly_mock: true do
expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
end
end
+
+ context "when a feature is not persisted" do
+ it 'returns false when opt_into_all_features is off' do
+ allow(Feature).to receive(:persisted?).and_return(false)
+ allow(described_class).to receive(:opt_into_all_features?).and_return(false)
+
+ expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(false)
+ end
+
+ it 'returns true when the override is on' do
+ allow(Feature).to receive(:persisted?).and_return(false)
+ allow(described_class).to receive(:opt_into_all_features?).and_return(true)
+
+ expect(described_class.feature_enabled?(feature_name, status: feature_status)).to be(true)
+ end
+ end
end
context 'when the feature_status is OPT_OUT' do
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index beed4e77e8b..3fb8edeb701 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -256,6 +256,7 @@ project:
- environments
- deployments
- project_feature
+- auto_devops
- pages_domains
- authorized_users
- project_authorizations
diff --git a/spec/lib/gitlab/import_export/safe_model_attributes.yml b/spec/lib/gitlab/import_export/safe_model_attributes.yml
index 1613b968bb6..899d17d97c2 100644
--- a/spec/lib/gitlab/import_export/safe_model_attributes.yml
+++ b/spec/lib/gitlab/import_export/safe_model_attributes.yml
@@ -223,6 +223,7 @@ Ci::Pipeline:
- lock_version
- auto_canceled_by_id
- pipeline_schedule_id
+- config_source
- protected
Ci::Stage:
- id
@@ -466,3 +467,10 @@ Timelog:
- user_id
- created_at
- updated_at
+ProjectAutoDevops:
+- id
+- enabled
+- domain
+- project_id
+- created_at
+- updated_at
diff --git a/spec/lib/gitlab/middleware/go_spec.rb b/spec/lib/gitlab/middleware/go_spec.rb
index 6af1564da19..cab662819ac 100644
--- a/spec/lib/gitlab/middleware/go_spec.rb
+++ b/spec/lib/gitlab/middleware/go_spec.rb
@@ -79,12 +79,28 @@ describe Gitlab::Middleware::Go do
it_behaves_like 'a nested project'
end
+ context 'with a subpackage that is not a valid project path' do
+ let(:path) { "#{project.full_path}/---subpackage" }
+
+ it_behaves_like 'a nested project'
+ end
+
context 'without subpackages' do
let(:path) { project.full_path }
it_behaves_like 'a nested project'
end
end
+
+ context 'with a bogus path' do
+ let(:path) { "http:;url=http:&sol;&sol;www.example.com'http-equiv='refresh'x='?go-get=1" }
+
+ it 'skips go-import generation' do
+ expect(app).to receive(:call).and_return('no-go')
+
+ go
+ end
+ end
end
def go
@@ -100,7 +116,7 @@ describe Gitlab::Middleware::Go do
def expect_response_with_path(response, path)
expect(response[0]).to eq(200)
expect(response[1]['Content-Type']).to eq('text/html')
- expected_body = "<!DOCTYPE html><html><head><meta content='#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git' name='go-import'></head></html>\n"
+ expected_body = %{<html><head><meta name="go-import" content="#{Gitlab.config.gitlab.host}/#{path} git http://#{Gitlab.config.gitlab.host}/#{path}.git" /></head></html>}
expect(response[2].body).to eq([expected_body])
end
end
diff --git a/spec/lib/gitlab/themes_spec.rb b/spec/lib/gitlab/themes_spec.rb
new file mode 100644
index 00000000000..ecacea6bb35
--- /dev/null
+++ b/spec/lib/gitlab/themes_spec.rb
@@ -0,0 +1,48 @@
+require 'spec_helper'
+
+describe Gitlab::Themes, lib: true do
+ describe '.body_classes' do
+ it 'returns a space-separated list of class names' do
+ css = described_class.body_classes
+
+ expect(css).to include('ui_indigo')
+ expect(css).to include(' ui_dark ')
+ expect(css).to include(' ui_blue')
+ end
+ end
+
+ describe '.by_id' do
+ it 'returns a Theme by its ID' do
+ expect(described_class.by_id(1).name).to eq 'Indigo'
+ expect(described_class.by_id(3).name).to eq 'Light'
+ end
+ end
+
+ describe '.default' do
+ it 'returns the default application theme' do
+ allow(described_class).to receive(:default_id).and_return(2)
+ expect(described_class.default.id).to eq 2
+ end
+
+ it 'prevents an infinite loop when configuration default is invalid' do
+ default = described_class::APPLICATION_DEFAULT
+ themes = described_class::THEMES
+
+ config = double(default_theme: 0).as_null_object
+ allow(Gitlab).to receive(:config).and_return(config)
+ expect(described_class.default.id).to eq default
+
+ config = double(default_theme: themes.size + 5).as_null_object
+ allow(Gitlab).to receive(:config).and_return(config)
+ expect(described_class.default.id).to eq default
+ end
+ end
+
+ describe '.each' do
+ it 'passes the block to the THEMES Array' do
+ ids = []
+ described_class.each { |theme| ids << theme.id }
+ expect(ids).not_to be_empty
+ end
+ end
+end
diff --git a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
index 1396d12e5a9..759e77ac9db 100644
--- a/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
+++ b/spec/migrations/convert_custom_notification_settings_to_columns_spec.rb
@@ -2,6 +2,8 @@ require 'spec_helper'
require Rails.root.join('db', 'post_migrate', '20170607121233_convert_custom_notification_settings_to_columns')
describe ConvertCustomNotificationSettingsToColumns, :migration do
+ let(:user_class) { table(:users) }
+
let(:settings_params) do
[
{ level: 0, events: [:new_note] }, # disabled, single event
@@ -19,7 +21,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
- user = create(:user)
+ user = build(:user).becomes(user_class).tap(&:save!)
create_params = { user_id: user.id, level: params[:level], events: events }
notification_setting = described_class::NotificationSetting.create(create_params)
@@ -35,7 +37,7 @@ describe ConvertCustomNotificationSettingsToColumns, :migration do
events[event] = true
end
- user = create(:user)
+ user = build(:user).becomes(user_class).tap(&:save!)
create_params = events.merge(user_id: user.id, level: params[:level])
notification_setting = described_class::NotificationSetting.create(create_params)
diff --git a/spec/models/application_setting_spec.rb b/spec/models/application_setting_spec.rb
index f921545668d..c7a9eabdf06 100644
--- a/spec/models/application_setting_spec.rb
+++ b/spec/models/application_setting_spec.rb
@@ -5,6 +5,7 @@ describe ApplicationSetting do
it { expect(setting).to be_valid }
it { expect(setting.uuid).to be_present }
+ it { expect(setting).to have_db_column(:auto_devops_enabled) }
describe 'validations' do
let(:http) { 'http://example.com' }
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index 08d22f166e4..c2c9f1c12d1 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1688,6 +1688,30 @@ describe Ci::Build do
{ key: 'secret', value: 'value', public: false }])
end
end
+
+ context 'when using auto devops' do
+ context 'and is enabled' do
+ before do
+ project.create_auto_devops!(enabled: true, domain: 'example.com')
+ end
+
+ it "includes AUTO_DEVOPS_DOMAIN" do
+ is_expected.to include(
+ { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
+ end
+ end
+
+ context 'and is disabled' do
+ before do
+ project.create_auto_devops!(enabled: false, domain: 'example.com')
+ end
+
+ it "includes AUTO_DEVOPS_DOMAIN" do
+ is_expected.not_to include(
+ { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
+ end
+ end
+ end
end
describe 'state transition: any => [:pending]' do
diff --git a/spec/models/ci/pipeline_spec.rb b/spec/models/ci/pipeline_spec.rb
index 84656ffe0b9..95da97b7bc5 100644
--- a/spec/models/ci/pipeline_spec.rb
+++ b/spec/models/ci/pipeline_spec.rb
@@ -799,14 +799,118 @@ describe Ci::Pipeline, :mailer do
end
end
+ describe '#set_config_source' do
+ context 'on object initialisation' do
+ context 'when pipelines does not contain needed data' do
+ let(:pipeline) do
+ Ci::Pipeline.new
+ end
+
+ it 'defines source to be unknown' do
+ expect(pipeline).to be_unknown_source
+ end
+ end
+
+ context 'when pipeline contains all needed data' do
+ let(:pipeline) do
+ Ci::Pipeline.new(
+ project: project,
+ sha: '1234',
+ ref: 'master',
+ source: :push)
+ end
+
+ context 'when the repository has a config file' do
+ before do
+ allow(project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
+ end
+
+ it 'defines source to be from repository' do
+ expect(pipeline).to be_repository_source
+ end
+
+ context 'when loading an object' do
+ let(:new_pipeline) { Ci::Pipeline.find(pipeline.id) }
+
+ it 'does not redefine the source' do
+ # force to overwrite the source
+ pipeline.unknown_source!
+
+ expect(new_pipeline).to be_unknown_source
+ end
+ end
+ end
+
+ context 'when the repository does not have a config file' do
+ let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
+
+ context 'auto devops enabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ allow(project).to receive(:ci_config_path) { 'custom' }
+ end
+
+ it 'defines source to be auto devops' do
+ subject
+
+ expect(pipeline).to be_auto_devops_source
+ end
+ end
+ end
+ end
+ end
+ end
+
describe '#ci_yaml_file' do
- it 'reports error if the file is not found' do
- allow(pipeline.project).to receive(:ci_config_path) { 'custom' }
+ let(:implied_yml) { Gitlab::Template::GitlabCiYmlTemplate.find('Auto-DevOps').content }
+
+ context 'the source is unknown' do
+ before do
+ pipeline.unknown_source!
+ end
+
+ it 'returns the configuration if found' do
+ allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
+
+ expect(pipeline.ci_yaml_file).to be_a(String)
+ expect(pipeline.ci_yaml_file).not_to eq(implied_yml)
+ expect(pipeline.yaml_errors).to be_nil
+ end
+
+ it 'sets yaml errors if not found' do
+ expect(pipeline.ci_yaml_file).to be_nil
+ expect(pipeline.yaml_errors)
+ .to start_with('Failed to load CI/CD config file')
+ end
+ end
+
+ context 'the source is the repository' do
+ before do
+ pipeline.repository_source!
+ end
- pipeline.ci_yaml_file
+ it 'returns the configuration if found' do
+ allow(pipeline.project.repository).to receive(:gitlab_ci_yml_for)
+ .and_return('config')
- expect(pipeline.yaml_errors)
- .to eq('Failed to load CI/CD config file at custom')
+ expect(pipeline.ci_yaml_file).to be_a(String)
+ expect(pipeline.ci_yaml_file).not_to eq(implied_yml)
+ expect(pipeline.yaml_errors).to be_nil
+ end
+ end
+
+ context 'when the source is auto_devops_source' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ pipeline.auto_devops_source!
+ end
+
+ it 'finds the implied config' do
+ expect(pipeline.ci_yaml_file).to eq(implied_yml)
+ expect(pipeline.yaml_errors).to be_nil
+ end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index ea8512a5eae..25e5d155894 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -54,6 +54,28 @@ describe Environment do
end
end
+ describe '#folder_name' do
+ context 'when it is inside a folder' do
+ subject(:environment) do
+ create(:environment, name: 'staging/review-1')
+ end
+
+ it 'returns a top-level folder name' do
+ expect(environment.folder_name).to eq 'staging'
+ end
+ end
+
+ context 'when the environment if a top-level item itself' do
+ subject(:environment) do
+ create(:environment, name: 'production')
+ end
+
+ it 'returns an environment name' do
+ expect(environment.folder_name).to eq 'production'
+ end
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index 69286eff984..81d5ab7a6d3 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -404,6 +404,118 @@ describe Namespace do
let!(:project1) { create(:project_empty_repo, namespace: group) }
let!(:project2) { create(:project_empty_repo, namespace: child) }
- it { expect(group.all_projects.to_a).to eq([project2, project1]) }
+ it { expect(group.all_projects.to_a).to match_array([project2, project1]) }
+ end
+
+ describe '#share_with_group_lock with subgroups', :nested_groups do
+ context 'when creating a subgroup' do
+ let(:subgroup) { create(:group, parent: root_group )}
+
+ context 'under a parent with "Share with group lock" enabled' do
+ let(:root_group) { create(:group, share_with_group_lock: true) }
+
+ it 'enables "Share with group lock" on the subgroup' do
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'under a parent with "Share with group lock" disabled' do
+ let(:root_group) { create(:group) }
+
+ it 'does not enable "Share with group lock" on the subgroup' do
+ expect(subgroup.share_with_group_lock).to be_falsey
+ end
+ end
+ end
+
+ context 'when enabling the parent group "Share with group lock"' do
+ let(:root_group) { create(:group) }
+ let!(:subgroup) { create(:group, parent: root_group )}
+
+ it 'the subgroup "Share with group lock" becomes enabled' do
+ root_group.update!(share_with_group_lock: true)
+
+ expect(subgroup.reload.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'when disabling the parent group "Share with group lock" (which was already enabled)' do
+ let(:root_group) { create(:group, share_with_group_lock: true) }
+
+ context 'and the subgroup "Share with group lock" is enabled' do
+ let(:subgroup) { create(:group, parent: root_group, share_with_group_lock: true )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ root_group.update!(share_with_group_lock: false)
+
+ expect(subgroup.reload.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'but the subgroup "Share with group lock" is disabled' do
+ let(:subgroup) { create(:group, parent: root_group )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ root_group.update!(share_with_group_lock: false)
+
+ expect(subgroup.reload.share_with_group_lock?).to be_falsey
+ end
+ end
+ end
+
+ # Note: Group transfers are not yet implemented
+ context 'when a group is transferred into a root group' do
+ context 'when the root group "Share with group lock" is enabled' do
+ let(:root_group) { create(:group, share_with_group_lock: true) }
+
+ context 'when the subgroup "Share with group lock" is enabled' do
+ let(:subgroup) { create(:group, share_with_group_lock: true )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'when the subgroup "Share with group lock" is disabled' do
+ let(:subgroup) { create(:group)}
+
+ it 'the subgroup "Share with group lock" becomes enabled' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+ end
+
+ context 'when the root group "Share with group lock" is disabled' do
+ let(:root_group) { create(:group) }
+
+ context 'when the subgroup "Share with group lock" is enabled' do
+ let(:subgroup) { create(:group, share_with_group_lock: true )}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_truthy
+ end
+ end
+
+ context 'when the subgroup "Share with group lock" is disabled' do
+ let(:subgroup) { create(:group)}
+
+ it 'the subgroup "Share with group lock" does not change' do
+ subgroup.parent = root_group
+ subgroup.save!
+
+ expect(subgroup.share_with_group_lock).to be_falsey
+ end
+ end
+ end
+ end
end
end
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
new file mode 100644
index 00000000000..ca13af4d73e
--- /dev/null
+++ b/spec/models/project_auto_devops_spec.rb
@@ -0,0 +1,23 @@
+require 'spec_helper'
+
+describe ProjectAutoDevops do
+ set(:project) { build(:project) }
+
+ it { is_expected.to belong_to(:project) }
+
+ it { is_expected.to respond_to(:created_at) }
+ it { is_expected.to respond_to(:updated_at) }
+
+ describe 'variables' do
+ let(:auto_devops) { build_stubbed(:project_auto_devops, project: project, domain: domain) }
+
+ context 'when domain is defined' do
+ let(:domain) { 'example.com' }
+
+ it 'returns AUTO_DEVOPS_DOMAIN' do
+ expect(auto_devops.variables).to include(
+ { key: 'AUTO_DEVOPS_DOMAIN', value: 'example.com', public: true })
+ end
+ end
+ end
+end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 1f7c6a82b91..48fc77423ff 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -53,6 +53,7 @@ describe Project do
it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
it { is_expected.to have_one(:last_event).class_name('Event') }
it { is_expected.to have_one(:forked_from_project).through(:forked_project_link) }
+ it { is_expected.to have_one(:auto_devops).class_name('ProjectAutoDevops') }
it { is_expected.to have_many(:commit_statuses) }
it { is_expected.to have_many(:pipelines) }
it { is_expected.to have_many(:builds) }
@@ -2508,4 +2509,177 @@ describe Project do
end
end
end
+
+ describe '#has_ci?' do
+ set(:project) { create(:project) }
+ let(:repository) { double }
+
+ before do
+ expect(project).to receive(:repository) { repository }
+ end
+
+ context 'when has .gitlab-ci.yml' do
+ before do
+ expect(repository).to receive(:gitlab_ci_yml) { 'content' }
+ end
+
+ it "CI is available" do
+ expect(project).to have_ci
+ end
+ end
+
+ context 'when there is no .gitlab-ci.yml' do
+ before do
+ expect(repository).to receive(:gitlab_ci_yml) { nil }
+ end
+
+ it "CI is not available" do
+ expect(project).not_to have_ci
+ end
+
+ context 'when auto devops is enabled' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it "CI is available" do
+ expect(project).to have_ci
+ end
+ end
+ end
+ end
+
+ describe '#auto_devops_enabled?' do
+ set(:project) { create(:project) }
+
+ subject { project.auto_devops_enabled? }
+
+ context 'when enabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it 'auto devops is implicitly enabled' do
+ expect(project.auto_devops).to be_nil
+ expect(project).to be_auto_devops_enabled
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it "auto devops is enabled" do
+ expect(project).to be_auto_devops_enabled
+ end
+ end
+
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it "auto devops is disabled" do
+ expect(project).not_to be_auto_devops_enabled
+ end
+ end
+ end
+
+ context 'when disabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it 'auto devops is implicitly disabled' do
+ expect(project.auto_devops).to be_nil
+ expect(project).not_to be_auto_devops_enabled
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it "auto devops is enabled" do
+ expect(project).to be_auto_devops_enabled
+ end
+ end
+ end
+ end
+
+ describe '#has_auto_devops_implicitly_disabled?' do
+ set(:project) { create(:project) }
+
+ context 'when enabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
+ context 'when disabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: false)
+ end
+
+ it 'auto devops is implicitly disabled' do
+ expect(project).to have_auto_devops_implicitly_disabled
+ end
+
+ context 'when explicitly disabled' do
+ before do
+ create(:project_auto_devops, project: project, enabled: false)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
+ context 'when explicitly enabled' do
+ before do
+ create(:project_auto_devops, project: project)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+ end
+ end
+
+ context '#auto_devops_variables' do
+ set(:project) { create(:project) }
+
+ subject { project.auto_devops_variables }
+
+ context 'when enabled in settings' do
+ before do
+ stub_application_setting(auto_devops_enabled: true)
+ end
+
+ context 'when domain is empty' do
+ before do
+ create(:project_auto_devops, project: project, domain: nil)
+ end
+
+ it 'variables are empty' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when domain is configured' do
+ before do
+ create(:project_auto_devops, project: project, domain: 'example.com')
+ end
+
+ it "variables are not empty" do
+ is_expected.not_to be_empty
+ end
+ end
+ end
+ end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index abf732e60bf..73a1e47149c 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -716,6 +716,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(Gitlab.config.gitlab.default_projects_limit)
expect(user.can_create_group).to eq(Gitlab.config.gitlab.default_can_create_group)
+ expect(user.theme_id).to eq(Gitlab.config.gitlab.default_theme)
expect(user.external).to be_falsey
end
end
@@ -726,6 +727,7 @@ describe User do
it "applies defaults to user" do
expect(user.projects_limit).to eq(123)
expect(user.can_create_group).to be_falsey
+ expect(user.theme_id).to eq(1)
end
end
@@ -1517,7 +1519,7 @@ describe User do
developer_project = create(:project) { |p| p.add_developer(user) }
master_project = create(:project) { |p| p.add_master(user) }
- expect(user.projects_where_can_admin_issues.to_a).to eq([master_project, developer_project, reporter_project])
+ expect(user.projects_where_can_admin_issues.to_a).to match_array([master_project, developer_project, reporter_project])
expect(user.can?(:admin_issue, master_project)).to eq(true)
expect(user.can?(:admin_issue, developer_project)).to eq(true)
expect(user.can?(:admin_issue, reporter_project)).to eq(true)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 7f832bfa563..b186a78e44a 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -24,8 +24,8 @@ describe GroupPolicy do
:admin_namespace,
:admin_group_member,
:change_visibility_level,
- :create_subgroup
- ]
+ (Gitlab::Database.postgresql? ? :create_subgroup : nil)
+ ].compact
end
before do
@@ -242,4 +242,94 @@ describe GroupPolicy do
end
end
end
+
+ describe 'change_share_with_group_lock' do
+ context 'when the current_user owns the group' do
+ let(:current_user) { owner }
+
+ context 'when the group share_with_group_lock is enabled' do
+ let(:group) { create(:group, share_with_group_lock: true, parent: parent) }
+
+ context 'when the parent group share_with_group_lock is enabled' do
+ context 'when the group has a grandparent' do
+ let(:parent) { create(:group, share_with_group_lock: true, parent: grandparent) }
+
+ context 'when the grandparent share_with_group_lock is enabled' do
+ let(:grandparent) { create(:group, share_with_group_lock: true) }
+
+ context 'when the current_user owns the parent' do
+ before do
+ parent.add_owner(current_user)
+ end
+
+ context 'when the current_user owns the grandparent' do
+ before do
+ grandparent.add_owner(current_user)
+ end
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+
+ context 'when the current_user does not own the grandparent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the current_user does not own the parent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the grandparent share_with_group_lock is disabled' do
+ let(:grandparent) { create(:group) }
+
+ context 'when the current_user owns the parent' do
+ before do
+ parent.add_owner(current_user)
+ end
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+
+ context 'when the current_user does not own the parent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+ end
+
+ context 'when the group does not have a grandparent' do
+ let(:parent) { create(:group, share_with_group_lock: true) }
+
+ context 'when the current_user owns the parent' do
+ before do
+ parent.add_owner(current_user)
+ end
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+
+ context 'when the current_user does not own the parent' do
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
+ end
+
+ context 'when the parent group share_with_group_lock is disabled' do
+ let(:parent) { create(:group) }
+
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the group share_with_group_lock is disabled' do
+ it { expect_allowed(:change_share_with_group_lock) }
+ end
+ end
+
+ context 'when the current_user does not own the group' do
+ let(:current_user) { create(:user) }
+
+ it { expect_disallowed(:change_share_with_group_lock) }
+ end
+ end
end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 77c43f92456..42f0079e173 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -431,6 +431,30 @@ describe API::Groups do
expect(response).to have_http_status(403)
end
+
+ context 'as owner', :nested_groups do
+ before do
+ group2.add_owner(user1)
+ end
+
+ it 'can create subgroups' do
+ post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo'
+
+ expect(response).to have_http_status(201)
+ end
+ end
+
+ context 'as master', :nested_groups do
+ before do
+ group2.add_master(user1)
+ end
+
+ it 'cannot create subgroups' do
+ post api("/groups", user1), parent_id: group2.id, name: 'foo', path: 'foo'
+
+ expect(response).to have_http_status(403)
+ end
+ end
end
context "when authenticated as user with group permissions" do
diff --git a/spec/requests/api/merge_request_diffs_spec.rb b/spec/requests/api/merge_request_diffs_spec.rb
index d1b22179888..d9da94d4713 100644
--- a/spec/requests/api/merge_request_diffs_spec.rb
+++ b/spec/requests/api/merge_request_diffs_spec.rb
@@ -14,7 +14,7 @@ describe API::MergeRequestDiffs, 'MergeRequestDiffs' do
describe 'GET /projects/:id/merge_requests/:merge_request_iid/versions' do
it 'returns 200 for a valid merge request' do
get api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/versions", user)
- merge_request_diff = merge_request.merge_request_diffs.first
+ merge_request_diff = merge_request.merge_request_diffs.last
expect(response.status).to eq 200
expect(response).to include_pagination_headers
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index f771e4fa4ff..9602584f546 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -595,7 +595,7 @@ describe API::Projects do
expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('foo-project')
@@ -606,7 +606,7 @@ describe API::Projects do
.to change { Project.count }.by(1)
expect(response).to have_http_status(201)
- project = Project.first
+ project = Project.last
expect(project.name).to eq('Foo Project')
expect(project.path).to eq('path-project-Foo')
diff --git a/spec/requests/api/v3/merge_request_diffs_spec.rb b/spec/requests/api/v3/merge_request_diffs_spec.rb
index 8020ddab4c8..3f21ff40726 100644
--- a/spec/requests/api/v3/merge_request_diffs_spec.rb
+++ b/spec/requests/api/v3/merge_request_diffs_spec.rb
@@ -14,7 +14,7 @@ describe API::V3::MergeRequestDiffs, 'MergeRequestDiffs' do
describe 'GET /projects/:id/merge_requests/:merge_request_id/versions' do
it 'returns 200 for a valid merge request' do
get v3_api("/projects/#{project.id}/merge_requests/#{merge_request.id}/versions", user)
- merge_request_diff = merge_request.merge_request_diffs.first
+ merge_request_diff = merge_request.merge_request_diffs.last
expect(response.status).to eq 200
expect(json_response.size).to eq(merge_request.merge_request_diffs.size)
diff --git a/spec/requests/projects/cycle_analytics_events_spec.rb b/spec/requests/projects/cycle_analytics_events_spec.rb
index e5d9d3df5a8..6667ce771bd 100644
--- a/spec/requests/projects/cycle_analytics_events_spec.rb
+++ b/spec/requests/projects/cycle_analytics_events_spec.rb
@@ -93,7 +93,7 @@ describe 'cycle analytics events' do
context 'with private project and builds' do
before do
- project.members.first.update(access_level: Gitlab::Access::GUEST)
+ project.members.last.update(access_level: Gitlab::Access::GUEST)
end
it 'does not list the test events' do
diff --git a/spec/serializers/environment_entity_spec.rb b/spec/serializers/environment_entity_spec.rb
index 979d9921941..8f32c5639a1 100644
--- a/spec/serializers/environment_entity_spec.rb
+++ b/spec/serializers/environment_entity_spec.rb
@@ -16,6 +16,10 @@ describe EnvironmentEntity do
expect(subject).to include(:id, :name, :state, :environment_path)
end
+ it 'exposes folder path' do
+ expect(subject).to include(:folder_path)
+ end
+
context 'metrics disabled' do
before do
allow(environment).to receive(:has_metrics?).and_return(false)
diff --git a/spec/serializers/pipeline_entity_spec.rb b/spec/serializers/pipeline_entity_spec.rb
index 881f2b6bfd8..f8df461bc81 100644
--- a/spec/serializers/pipeline_entity_spec.rb
+++ b/spec/serializers/pipeline_entity_spec.rb
@@ -36,7 +36,7 @@ describe PipelineEntity do
it 'contains flags' do
expect(subject).to include :flags
expect(subject[:flags])
- .to include :latest, :stuck,
+ .to include :latest, :stuck, :auto_devops,
:yaml_errors, :retryable, :cancelable
end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 49d7c663128..4c2ff08039c 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Ci::CreatePipelineService do
- let(:project) { create(:project, :repository) }
+ set(:project) { create(:project, :repository) }
let(:user) { create(:admin) }
let(:ref_name) { 'refs/heads/master' }
@@ -489,7 +489,7 @@ describe Ci::CreatePipelineService do
subject do
described_class.new(project, user, ref: ref)
- .send(:allowed_to_create?, user)
+ .send(:allowed_to_create?)
end
context 'when user is a developer' do
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index 10dda45d2a1..224e933bebc 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -22,6 +22,26 @@ describe Groups::CreateService, '#execute' do
end
end
+ describe 'creating a top level group' do
+ let(:service) { described_class.new(user, group_params) }
+
+ context 'when user can create a group' do
+ before do
+ user.update_attribute(:can_create_group, true)
+ end
+
+ it { is_expected.to be_persisted }
+ end
+
+ context 'when user can not create a group' do
+ before do
+ user.update_attribute(:can_create_group, false)
+ end
+
+ it { is_expected.not_to be_persisted }
+ end
+ end
+
describe 'creating subgroup', :nested_groups do
let!(:group) { create(:group) }
let!(:service) { described_class.new(user, group_params.merge(parent_id: group.id)) }
@@ -44,13 +64,26 @@ describe Groups::CreateService, '#execute' do
end
end
- context 'as guest' do
- it 'does not save group and returns an error' do
+ context 'when nested groups feature is enabled' do
+ before do
allow(Group).to receive(:supports_nested_groups?).and_return(true)
+ end
+
+ context 'as guest' do
+ it 'does not save group and returns an error' do
+ is_expected.not_to be_persisted
+
+ expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.')
+ expect(subject.parent_id).to be_nil
+ end
+ end
+
+ context 'as owner' do
+ before do
+ group.add_owner(user)
+ end
- is_expected.not_to be_persisted
- expect(subject.errors[:parent_id].first).to eq('You don’t have permission to create a subgroup in this group.')
- expect(subject.parent_id).to be_nil
+ it { is_expected.to be_persisted }
end
end
end
diff --git a/spec/services/groups/update_service_spec.rb b/spec/services/groups/update_service_spec.rb
index 44f22a3b37b..1737fd0a9fc 100644
--- a/spec/services/groups/update_service_spec.rb
+++ b/spec/services/groups/update_service_spec.rb
@@ -100,4 +100,38 @@ describe Groups::UpdateService do
end
end
end
+
+ context 'for a subgroup', :nested_groups do
+ let(:subgroup) { create(:group, :private, parent: private_group) }
+
+ context 'when the parent group share_with_group_lock is enabled' do
+ before do
+ private_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'for the parent group owner' do
+ it 'allows disabling share_with_group_lock' do
+ private_group.add_owner(user)
+
+ result = described_class.new(subgroup, user, share_with_group_lock: false).execute
+
+ expect(result).to be_truthy
+ expect(subgroup.reload.share_with_group_lock).to be_falsey
+ end
+ end
+
+ context 'for a subgroup owner (who does not own the parent)' do
+ it 'does not allow disabling share_with_group_lock' do
+ subgroup_owner = create(:user)
+ subgroup.add_owner(subgroup_owner)
+
+ result = described_class.new(subgroup, subgroup_owner, share_with_group_lock: false).execute
+
+ expect(result).to be_falsey
+ expect(subgroup.errors.full_messages.first).to match(/cannot be disabled when the parent group "Share with group lock" is enabled, except by the owner of the parent group/)
+ expect(subgroup.reload.share_with_group_lock).to be_truthy
+ end
+ end
+ end
+ end
end
diff --git a/spec/services/notification_service_spec.rb b/spec/services/notification_service_spec.rb
index 5b349017c8b..3e493148b32 100644
--- a/spec/services/notification_service_spec.rb
+++ b/spec/services/notification_service_spec.rb
@@ -1237,7 +1237,7 @@ describe NotificationService, :mailer do
end
it do
- group_member = group.members.first
+ group_member = group.members.last
expect do
notification.decline_group_invite(group_member)
@@ -1285,7 +1285,7 @@ describe NotificationService, :mailer do
end
it do
- project_member = project.members.first
+ project_member = project.members.last
expect do
notification.decline_project_invite(project_member)
diff --git a/spec/services/projects/housekeeping_service_spec.rb b/spec/services/projects/housekeeping_service_spec.rb
index 9386c110385..437c009e7fa 100644
--- a/spec/services/projects/housekeeping_service_spec.rb
+++ b/spec/services/projects/housekeeping_service_spec.rb
@@ -20,6 +20,7 @@ describe Projects::HousekeepingService do
expect(GitGarbageCollectWorker).to receive(:perform_async).with(project.id, :the_task, :the_lease_key, :the_uuid)
subject.execute
+
expect(project.reload.pushes_since_gc).to eq(0)
end
diff --git a/spec/services/test_hooks/project_service_spec.rb b/spec/services/test_hooks/project_service_spec.rb
index 4218c15a3ce..28dfa9cf59c 100644
--- a/spec/services/test_hooks/project_service_spec.rb
+++ b/spec/services/test_hooks/project_service_spec.rb
@@ -21,6 +21,7 @@ describe TestHooks::ProjectService do
context 'push_events' do
let(:trigger) { 'push_events' }
+ let(:trigger_key) { :push_hooks }
it 'returns error message if not enough data' do
allow(project).to receive(:empty_repo?).and_return(true)
@@ -33,13 +34,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:empty_repo?).and_return(false)
allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'tag_push_events' do
let(:trigger) { 'tag_push_events' }
+ let(:trigger_key) { :tag_push_hooks }
it 'returns error message if not enough data' do
allow(project).to receive(:empty_repo?).and_return(true)
@@ -52,13 +54,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:empty_repo?).and_return(false)
allow(Gitlab::DataBuilder::Push).to receive(:build_sample).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'note_events' do
let(:trigger) { 'note_events' }
+ let(:trigger_key) { :note_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -69,13 +72,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:notes).and_return([Note.new])
allow(Gitlab::DataBuilder::Note).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'issues_events' do
let(:trigger) { 'issues_events' }
+ let(:trigger_key) { :issue_hooks }
let(:issue) { build(:issue) }
it 'returns error message if not enough data' do
@@ -87,13 +91,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:issues).and_return([issue])
allow(issue).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'confidential_issues_events' do
let(:trigger) { 'confidential_issues_events' }
+ let(:trigger_key) { :confidential_issue_hooks }
let(:issue) { build(:issue) }
it 'returns error message if not enough data' do
@@ -105,13 +110,14 @@ describe TestHooks::ProjectService do
allow(project).to receive(:issues).and_return([issue])
allow(issue).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'merge_requests_events' do
let(:trigger) { 'merge_requests_events' }
+ let(:trigger_key) { :merge_request_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -122,13 +128,14 @@ describe TestHooks::ProjectService do
create(:merge_request, source_project: project)
allow_any_instance_of(MergeRequest).to receive(:to_hook_data).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'job_events' do
let(:trigger) { 'job_events' }
+ let(:trigger_key) { :job_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -139,13 +146,14 @@ describe TestHooks::ProjectService do
create(:ci_build, project: project)
allow(Gitlab::DataBuilder::Build).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'pipeline_events' do
let(:trigger) { 'pipeline_events' }
+ let(:trigger_key) { :pipeline_hooks }
it 'returns error message if not enough data' do
expect(hook).not_to receive(:execute)
@@ -156,13 +164,14 @@ describe TestHooks::ProjectService do
create(:ci_empty_pipeline, project: project)
allow(Gitlab::DataBuilder::Pipeline).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'wiki_page_events' do
let(:trigger) { 'wiki_page_events' }
+ let(:trigger_key) { :wiki_page_hooks }
it 'returns error message if wiki disabled' do
allow(project).to receive(:wiki_enabled?).and_return(false)
@@ -180,7 +189,7 @@ describe TestHooks::ProjectService do
create(:wiki_page, wiki: project.wiki)
allow(Gitlab::DataBuilder::WikiPage).to receive(:build).and_return(sample_data)
- expect(hook).to receive(:execute).with(sample_data, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(sample_data, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
diff --git a/spec/services/test_hooks/system_service_spec.rb b/spec/services/test_hooks/system_service_spec.rb
index a15708bf82f..ff8b9595538 100644
--- a/spec/services/test_hooks/system_service_spec.rb
+++ b/spec/services/test_hooks/system_service_spec.rb
@@ -24,36 +24,39 @@ describe TestHooks::SystemService do
context 'push_events' do
let(:trigger) { 'push_events' }
+ let(:trigger_key) { :push_hooks }
it 'executes hook' do
allow(project).to receive(:empty_repo?).and_return(false)
expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'tag_push_events' do
let(:trigger) { 'tag_push_events' }
+ let(:trigger_key) { :tag_push_hooks }
it 'executes hook' do
allow(project.repository).to receive(:tags).and_return(['tag'])
expect(Gitlab::DataBuilder::Push).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Push::SAMPLE_DATA, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
context 'repository_update_events' do
let(:trigger) { 'repository_update_events' }
+ let(:trigger_key) { :repository_update_hooks }
it 'executes hook' do
allow(project).to receive(:empty_repo?).and_return(false)
expect(Gitlab::DataBuilder::Repository).to receive(:sample_data).and_call_original
- expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger).and_return(success_result)
+ expect(hook).to receive(:execute).with(Gitlab::DataBuilder::Repository::SAMPLE_DATA, trigger_key).and_return(success_result)
expect(service.execute).to include(success_result)
end
end
diff --git a/spec/services/web_hook_service_spec.rb b/spec/services/web_hook_service_spec.rb
index 0726e135b20..a669429ce3e 100644
--- a/spec/services/web_hook_service_spec.rb
+++ b/spec/services/web_hook_service_spec.rb
@@ -12,7 +12,7 @@ describe WebHookService do
let(:data) do
{ before: 'oldrev', after: 'newrev', ref: 'ref' }
end
- let(:service_instance) { described_class.new(project_hook, data, 'push_hooks') }
+ let(:service_instance) { described_class.new(project_hook, data, :push_hooks) }
describe '#execute' do
before do
diff --git a/spec/support/controllers/githubish_import_controller_shared_examples.rb b/spec/support/controllers/githubish_import_controller_shared_examples.rb
index 4eec3127464..b23d81a226a 100644
--- a/spec/support/controllers/githubish_import_controller_shared_examples.rb
+++ b/spec/support/controllers/githubish_import_controller_shared_examples.rb
@@ -140,9 +140,14 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context "when a namespace with the provider user's username already exists" do
- let!(:existing_namespace) { create(:namespace, name: other_username, owner: user) }
+ let!(:existing_namespace) { user.namespace }
context "when the namespace is owned by the GitLab user" do
+ before do
+ user.username = other_username
+ user.save
+ end
+
it "takes the existing namespace" do
expect(Gitlab::GithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, existing_namespace, user, access_params, type: provider)
@@ -153,12 +158,9 @@ shared_examples 'a GitHub-ish import controller: POST create' do
end
context "when the namespace is not owned by the GitLab user" do
- before do
- existing_namespace.owner = create(:user)
- existing_namespace.save
- end
-
it "creates a project using user's namespace" do
+ create(:user, username: other_username)
+
expect(Gitlab::GithubImport::ProjectCreator)
.to receive(:new).with(provider_repo, provider_repo.name, user.namespace, user, access_params, type: provider)
.and_return(double(execute: true))
diff --git a/spec/support/gitlab_stubs/session.json b/spec/support/gitlab_stubs/session.json
index cd55d63125e..688175369ae 100644
--- a/spec/support/gitlab_stubs/session.json
+++ b/spec/support/gitlab_stubs/session.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "color_scheme_id":2,
+ "theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
diff --git a/spec/support/gitlab_stubs/user.json b/spec/support/gitlab_stubs/user.json
index cd55d63125e..ce8dfe5ae75 100644
--- a/spec/support/gitlab_stubs/user.json
+++ b/spec/support/gitlab_stubs/user.json
@@ -7,7 +7,7 @@
"skype":"aertert",
"linkedin":"",
"twitter":"",
- "color_scheme_id":2,
+ "theme_id":2,"color_scheme_id":2,
"state":"active",
"created_at":"2012-12-21T13:02:20Z",
"extern_uid":null,
@@ -17,4 +17,4 @@
"can_create_project":false,
"private_token":"Wvjy2Krpb7y8xi93owUz",
"access_token":"Wvjy2Krpb7y8xi93owUz"
-}
+} \ No newline at end of file
diff --git a/spec/views/groups/edit.html.haml_spec.rb b/spec/views/groups/edit.html.haml_spec.rb
new file mode 100644
index 00000000000..38cfb84f0d5
--- /dev/null
+++ b/spec/views/groups/edit.html.haml_spec.rb
@@ -0,0 +1,116 @@
+require 'spec_helper'
+
+describe 'groups/edit.html.haml' do
+ include Devise::Test::ControllerHelpers
+
+ describe '"Share with group lock" setting' do
+ let(:root_owner) { create(:user) }
+ let(:root_group) { create(:group) }
+
+ before do
+ root_group.add_owner(root_owner)
+ end
+
+ shared_examples_for '"Share with group lock" setting' do |checkbox_options|
+ it 'should have the correct label, help text, and checkbox options' do
+ assign(:group, test_group)
+ allow(view).to receive(:can?).with(test_user, :admin_group, test_group).and_return(true)
+ allow(view).to receive(:can_change_group_visibility_level?).and_return(false)
+ allow(view).to receive(:current_user).and_return(test_user)
+ expect(view).to receive(:can_change_share_with_group_lock?).and_return(!checkbox_options[:disabled])
+ expect(view).to receive(:share_with_group_lock_help_text).and_return('help text here')
+
+ render
+
+ expect(rendered).to have_content("Prevent sharing a project within #{test_group.name} with other groups")
+ expect(rendered).to have_css('.descr', text: 'help text here')
+ expect(rendered).to have_field('group_share_with_group_lock', checkbox_options)
+ end
+ end
+
+ context 'for a root group' do
+ let(:test_group) { root_group }
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'for a subgroup', :nested_groups do
+ let!(:subgroup) { create(:group, parent: root_group) }
+ let(:sub_owner) { create(:user) }
+ let(:test_group) { subgroup }
+
+ context 'when the root_group has "Share with group lock" disabled' do
+ context 'when the subgroup has "Share with group lock" disabled' do
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+ end
+ end
+
+ context 'when the root_group has "Share with group lock" enabled' do
+ before do
+ root_group.update_column(:share_with_group_lock, true)
+ end
+
+ context 'when the subgroup has "Share with group lock" disabled (parent overridden)' do
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: false }
+ end
+ end
+
+ context 'when the subgroup has "Share with group lock" enabled (same as parent)' do
+ before do
+ subgroup.update_column(:share_with_group_lock, true)
+ end
+
+ context 'as the root_owner' do
+ let(:test_user) { root_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: false, checked: true }
+ end
+
+ context 'as the sub_owner' do
+ let(:test_user) { sub_owner }
+
+ it_behaves_like '"Share with group lock" setting', { disabled: true, checked: true }
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/views/projects/edit.html.haml_spec.rb b/spec/views/projects/edit.html.haml_spec.rb
index c1398629749..5c6b2e4b042 100644
--- a/spec/views/projects/edit.html.haml_spec.rb
+++ b/spec/views/projects/edit.html.haml_spec.rb
@@ -15,17 +15,6 @@ describe 'projects/edit' do
current_application_settings: Gitlab::CurrentSettings.current_application_settings)
end
- context 'LFS enabled setting' do
- it 'displays the correct elements' do
- allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
-
- render
-
- expect(rendered).to have_select('project_lfs_enabled')
- expect(rendered).to have_content('Git Large File Storage')
- end
- end
-
context 'project export disabled' do
it 'does not display the project export option' do
stub_application_setting(project_export_enabled?: false)
diff --git a/spec/workers/git_garbage_collect_worker_spec.rb b/spec/workers/git_garbage_collect_worker_spec.rb
index c4979792194..6f9ddb6c63c 100644
--- a/spec/workers/git_garbage_collect_worker_spec.rb
+++ b/spec/workers/git_garbage_collect_worker_spec.rb
@@ -5,28 +5,100 @@ require 'spec_helper'
describe GitGarbageCollectWorker do
let(:project) { create(:project, :repository) }
let(:shell) { Gitlab::Shell.new }
+ let!(:lease_uuid) { SecureRandom.uuid }
+ let!(:lease_key) { "project_housekeeping:#{project.id}" }
subject { described_class.new }
describe "#perform" do
shared_examples 'flushing ref caches' do |gitaly|
- it "flushes ref caches when the task if 'gc'" do
- expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
-
- if gitaly
- expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
- .and_return(nil)
- else
- expect(Gitlab::Popen).to receive(:popen)
- .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+ context 'with active lease_uuid' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
end
- expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
- expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
- expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:renew_lease).with(lease_key, lease_uuid).and_call_original
+ expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
- subject.perform(project.id)
+ if gitaly
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
+ .and_return(nil)
+ else
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+ end
+
+ expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id, :gc, lease_key, lease_uuid)
+ end
+ end
+
+ context 'with different lease than the active one' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(SecureRandom.uuid)
+ end
+
+ it 'returns silently' do
+ expect(subject).not_to receive(:command)
+ expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id, :gc, lease_key, lease_uuid)
+ end
+ end
+
+ context 'with no active lease' do
+ before do
+ allow(subject).to receive(:get_lease_uuid).and_return(false)
+ end
+
+ context 'when is able to get the lease' do
+ before do
+ allow(subject).to receive(:try_obtain_lease).and_return(SecureRandom.uuid)
+ end
+
+ it "flushes ref caches when the task if 'gc'" do
+ expect(subject).to receive(:command).with(:gc).and_return([:the, :command])
+
+ if gitaly
+ expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:garbage_collect)
+ .and_return(nil)
+ else
+ expect(Gitlab::Popen).to receive(:popen)
+ .with([:the, :command], project.repository.path_to_repo).and_return(["", 0])
+ end
+
+ expect_any_instance_of(Repository).to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).to receive(:branch_names).and_call_original
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(:branch_count).and_call_original
+ expect_any_instance_of(Gitlab::Git::Repository).to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id)
+ end
+ end
+
+ context 'when no lease can be obtained' do
+ before do
+ expect(subject).to receive(:try_obtain_lease).and_return(false)
+ end
+
+ it 'returns silently' do
+ expect(subject).not_to receive(:command)
+ expect_any_instance_of(Repository).not_to receive(:after_create_branch).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:branch_names).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:branch_count).and_call_original
+ expect_any_instance_of(Repository).not_to receive(:has_visible_content?).and_call_original
+
+ subject.perform(project.id)
+ end
+ end
end
end
@@ -39,25 +111,34 @@ describe GitGarbageCollectWorker do
end
context "repack_full" do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_full)
.and_return(nil)
- subject.perform(project.id, :full_repack)
+ subject.perform(project.id, :full_repack, lease_key, lease_uuid)
end
end
context "repack_incremental" do
+ before do
+ expect(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
+ end
+
it "calls Gitaly" do
expect_any_instance_of(Gitlab::GitalyClient::RepositoryService).to receive(:repack_incremental)
.and_return(nil)
- subject.perform(project.id, :incremental_repack)
+ subject.perform(project.id, :incremental_repack, lease_key, lease_uuid)
end
end
shared_examples 'gc tasks' do
before do
+ allow(subject).to receive(:get_lease_uuid).and_return(lease_uuid)
allow(subject).to receive(:bitmaps_enabled?).and_return(bitmaps_enabled)
end
@@ -67,7 +148,7 @@ describe GitGarbageCollectWorker do
expect(before_packs.count).to be >= 1
- subject.perform(project.id, 'incremental_repack')
+ subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
after_packs = packs(project)
# Exactly one new pack should have been created
@@ -79,12 +160,12 @@ describe GitGarbageCollectWorker do
it 'full repack consolidates into 1 packfile' do
create_objects(project)
- subject.perform(project.id, 'incremental_repack')
+ subject.perform(project.id, 'incremental_repack', lease_key, lease_uuid)
before_packs = packs(project)
expect(before_packs.count).to be >= 2
- subject.perform(project.id, 'full_repack')
+ subject.perform(project.id, 'full_repack', lease_key, lease_uuid)
after_packs = packs(project)
expect(after_packs.count).to eq(1)
@@ -102,7 +183,7 @@ describe GitGarbageCollectWorker do
expect(before_packs.count).to be >= 1
- subject.perform(project.id, 'gc')
+ subject.perform(project.id, 'gc', lease_key, lease_uuid)
after_packed_refs = packed_refs(project)
after_packs = packs(project)
diff --git a/spec/workers/post_receive_spec.rb b/spec/workers/post_receive_spec.rb
index af6a3c9f6c7..d3707a3cc11 100644
--- a/spec/workers/post_receive_spec.rb
+++ b/spec/workers/post_receive_spec.rb
@@ -127,6 +127,7 @@ describe PostReceive do
it "asks the project to trigger all hooks" do
allow(Project).to receive(:find_by).and_return(project)
+
expect(project).to receive(:execute_hooks).twice
expect(project).to receive(:execute_services).twice
@@ -135,6 +136,7 @@ describe PostReceive do
it "enqueues a UpdateMergeRequestsWorker job" do
allow(Project).to receive(:find_by).and_return(project)
+
expect(UpdateMergeRequestsWorker).to receive(:perform_async).with(project.id, project.owner.id, any_args)
described_class.new.perform(gl_repository, key_id, base64_changes)
diff --git a/vendor/project_templates/rails.tar.gz b/vendor/project_templates/rails.tar.gz
index 92b9860fbc0..561b1e5902c 100644
--- a/vendor/project_templates/rails.tar.gz
+++ b/vendor/project_templates/rails.tar.gz
Binary files differ