summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitignore1
-rw-r--r--.gitlab-ci.yml98
-rw-r--r--Gemfile4
-rw-r--r--Gemfile.lock22
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_canceled.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_created.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_failed.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_manual.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_not_found.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_pending.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_running.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_skipped.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_success.icobin0 -> 4286 bytes
-rw-r--r--app/assets/images/ci_favicons/dev/favicon_status_warning.icobin0 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_canceled.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_created.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_failed.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_manual.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_not_found.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_pending.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_running.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_skipped.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_success.icobin5430 -> 4286 bytes
-rw-r--r--[-rwxr-xr-x]app/assets/images/ci_favicons/favicon_status_warning.icobin5430 -> 4286 bytes
-rw-r--r--app/assets/javascripts/blob/pdf/index.js10
-rw-r--r--app/assets/javascripts/dispatcher.js12
-rw-r--r--app/assets/javascripts/landing.js37
-rw-r--r--app/assets/javascripts/monitoring/constants.js4
-rw-r--r--app/assets/javascripts/monitoring/deployments.js211
-rw-r--r--app/assets/javascripts/monitoring/prometheus_graph.js71
-rw-r--r--app/assets/javascripts/pdf/assets/img/bg.gifbin0 -> 58 bytes
-rw-r--r--app/assets/javascripts/pdf/index.vue73
-rw-r--r--app/assets/javascripts/pdf/page/index.vue68
-rw-r--r--app/assets/stylesheets/framework/blocks.scss57
-rw-r--r--app/assets/stylesheets/framework/common.scss5
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss44
-rw-r--r--app/assets/stylesheets/pages/environments.scss29
-rw-r--r--app/assets/stylesheets/pages/groups.scss23
-rw-r--r--app/controllers/admin/hooks_controller.rb26
-rw-r--r--app/controllers/concerns/notes_actions.rb136
-rw-r--r--app/controllers/concerns/renders_notes.rb2
-rw-r--r--app/controllers/concerns/snippets_actions.rb4
-rw-r--r--app/controllers/concerns/toggle_award_emoji.rb3
-rw-r--r--app/controllers/projects/deployments_controller.rb18
-rw-r--r--app/controllers/projects/hooks_controller.rb13
-rw-r--r--app/controllers/projects/notes_controller.rb139
-rw-r--r--app/controllers/snippets/notes_controller.rb44
-rw-r--r--app/controllers/snippets_controller.rb20
-rw-r--r--app/finders/notes_finder.rb2
-rw-r--r--app/helpers/award_emoji_helper.rb8
-rw-r--r--app/helpers/blob_helper.rb14
-rw-r--r--app/helpers/merge_requests_helper.rb6
-rw-r--r--app/helpers/snippets_helper.rb8
-rw-r--r--app/models/concerns/cache_markdown_field.rb3
-rw-r--r--app/models/merge_request.rb7
-rw-r--r--app/models/namespace.rb6
-rw-r--r--app/models/project.rb15
-rw-r--r--app/models/repository.rb8
-rw-r--r--app/models/user.rb2
-rw-r--r--app/serializers/deployment_entity.rb2
-rw-r--r--app/serializers/deployment_serializer.rb8
-rw-r--r--app/serializers/status_entity.rb5
-rw-r--r--app/services/merge_requests/build_service.rb2
-rw-r--r--app/services/todo_service.rb2
-rw-r--r--app/validators/dynamic_path_validator.rb208
-rw-r--r--app/validators/namespace_validator.rb73
-rw-r--r--app/validators/project_path_validator.rb35
-rw-r--r--app/views/admin/dashboard/index.html.haml6
-rw-r--r--app/views/admin/hooks/_form.html.haml40
-rw-r--r--app/views/admin/hooks/edit.html.haml14
-rw-r--r--app/views/admin/hooks/index.html.haml55
-rw-r--r--app/views/dashboard/_groups_head.html.haml6
-rw-r--r--app/views/discussions/_notes.html.haml2
-rw-r--r--app/views/explore/groups/index.html.haml9
-rw-r--r--app/views/projects/blob/_header.html.haml4
-rw-r--r--app/views/projects/empty.html.haml2
-rw-r--r--app/views/projects/environments/metrics.html.haml2
-rw-r--r--app/views/projects/hooks/_index.html.haml24
-rw-r--r--app/views/projects/hooks/edit.html.haml14
-rw-r--r--app/views/projects/merge_requests/_new_compare.html.haml2
-rw-r--r--app/views/projects/notes/_actions.html.haml44
-rw-r--r--app/views/projects/notes/_edit.html.haml3
-rw-r--r--app/views/projects/notes/_note.html.haml101
-rw-r--r--app/views/projects/notes/_notes_with_form.html.haml2
-rw-r--r--app/views/projects/settings/_head.html.haml2
-rw-r--r--app/views/projects/settings/integrations/_project_hook.html.haml1
-rw-r--r--app/views/shared/icons/_icon_explore_groups_splash.svg1
-rw-r--r--app/views/shared/notes/_note.html.haml62
-rw-r--r--app/views/shared/notes/_notes.html.haml (renamed from app/views/projects/notes/_notes.html.haml)4
-rw-r--r--app/views/shared/snippets/_blob.html.haml5
-rw-r--r--app/views/shared/web_hooks/_form.html.haml182
-rw-r--r--app/views/snippets/notes/_actions.html.haml13
-rw-r--r--app/views/snippets/notes/_edit.html.haml0
-rw-r--r--app/views/snippets/notes/_notes.html.haml2
-rw-r--r--app/views/snippets/show.html.haml5
-rw-r--r--app/workers/expire_build_instance_artifacts_worker.rb2
-rw-r--r--changelogs/unreleased/12910-personal-snippets-notes-show.yml4
-rw-r--r--changelogs/unreleased/19364-webhook-edit.yml4
-rw-r--r--changelogs/unreleased/26488-target-disabled-mr.yml4
-rw-r--r--changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml4
-rw-r--r--changelogs/unreleased/30272-bvl-reject-more-namespaces.yml4
-rw-r--r--changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml4
-rw-r--r--changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml4
-rw-r--r--changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml4
-rw-r--r--changelogs/unreleased/add-tanuki-ci-status-favicons.yml4
-rw-r--r--changelogs/unreleased/dm-blob-download-button.yml4
-rw-r--r--changelogs/unreleased/dm-snippet-download-button.yml4
-rw-r--r--changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml5
-rw-r--r--config/initializers/active_record_query_trace.rb5
-rw-r--r--config/initializers/carrierwave.rb2
-rw-r--r--config/routes/admin.rb6
-rw-r--r--config/routes/project.rb6
-rw-r--r--config/routes/snippets.rb11
-rw-r--r--config/webpack.config.js5
-rw-r--r--db/migrate/20170327091750_add_created_at_index_to_deployments.rb15
-rw-r--r--db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb55
-rw-r--r--db/schema.rb1
-rw-r--r--doc/administration/high_availability/load_balancer.md2
-rw-r--r--doc/administration/integration/terminal.md4
-rw-r--r--doc/development/migration_style_guide.md25
-rw-r--r--doc/gitlab-basics/create-project.md36
-rw-r--r--doc/gitlab-basics/img/create_new_project_button.pngbin6978 -> 3702 bytes
-rw-r--r--doc/install/installation.md11
-rw-r--r--doc/update/8.10-to-8.11.md2
-rw-r--r--doc/update/8.11-to-8.12.md2
-rw-r--r--doc/update/8.12-to-8.13.md2
-rw-r--r--doc/update/8.13-to-8.14.md2
-rw-r--r--features/steps/project/source/browse_files.rb2
-rw-r--r--lib/api/merge_requests.rb2
-rw-r--r--lib/api/v3/merge_requests.rb2
-rw-r--r--lib/constraints/group_url_constrainer.rb10
-rw-r--r--lib/constraints/project_url_constrainer.rb4
-rw-r--r--lib/gitlab/database/migration_helpers.rb23
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1.rb35
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb76
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb131
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb72
-rw-r--r--lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb45
-rw-r--r--lib/gitlab/email/receiver.rb2
-rw-r--r--lib/gitlab/etag_caching/router.rb24
-rw-r--r--lib/gitlab/git/repository.rb29
-rw-r--r--lib/gitlab/gitaly_client/ref.rb8
-rw-r--r--lib/gitlab/regex.rb4
-rw-r--r--lib/gitlab/user_access.rb4
-rw-r--r--lib/tasks/gitlab/gitaly.rake6
-rw-r--r--lib/tasks/gitlab/workhorse.rake6
-rw-r--r--package.json2
-rwxr-xr-xscripts/lint-doc.sh1
-rwxr-xr-xscripts/notify_slack.sh13
-rwxr-xr-xscripts/static-analysis40
-rw-r--r--spec/controllers/projects/deployments_controller_spec.rb42
-rw-r--r--spec/controllers/projects/notes_controller_spec.rb41
-rw-r--r--spec/controllers/snippets/notes_controller_spec.rb196
-rw-r--r--spec/controllers/snippets_controller_spec.rb180
-rw-r--r--spec/factories/notes.rb2
-rw-r--r--spec/factories/project_hooks.rb2
-rw-r--r--spec/factories/projects.rb4
-rw-r--r--spec/features/admin/admin_hooks_spec.rb43
-rw-r--r--spec/features/admin/admin_requests_profiles_spec.rb69
-rw-r--r--spec/features/explore/groups_list_spec.rb28
-rw-r--r--spec/features/merge_requests/create_new_mr_spec.rb1
-rw-r--r--spec/features/projects/blobs/blob_show_spec.rb26
-rw-r--r--spec/features/projects/commit/cherry_pick_spec.rb6
-rw-r--r--spec/features/projects/environments/environment_spec.rb2
-rw-r--r--spec/features/projects/merge_request_button_spec.rb4
-rw-r--r--spec/features/projects/settings/integration_settings_spec.rb94
-rw-r--r--spec/features/projects/snippets/show_spec.rb12
-rw-r--r--spec/features/security/project/internal_access_spec.rb15
-rw-r--r--spec/features/security/project/private_access_spec.rb15
-rw-r--r--spec/features/security/project/public_access_spec.rb15
-rw-r--r--spec/features/snippets/notes_on_personal_snippets_spec.rb39
-rw-r--r--spec/features/snippets/show_spec.rb12
-rw-r--r--spec/finders/notes_finder_spec.rb9
-rw-r--r--spec/fixtures/api/schemas/deployments.json58
-rw-r--r--spec/helpers/award_emoji_helper_spec.rb61
-rw-r--r--spec/helpers/merge_requests_helper_spec.rb46
-rw-r--r--spec/javascripts/blob/pdf/index_spec.js6
-rw-r--r--spec/javascripts/blob/pdf/test.pdfbin11956 -> 0 bytes
-rw-r--r--spec/javascripts/fixtures/environments.rb30
-rw-r--r--spec/javascripts/fixtures/environments/metrics.html.haml62
-rw-r--r--spec/javascripts/fixtures/pdf.rb18
-rw-r--r--spec/javascripts/landing_spec.js160
-rw-r--r--spec/javascripts/monitoring/deployments_spec.js133
-rw-r--r--spec/javascripts/monitoring/prometheus_graph_spec.js4
-rw-r--r--spec/javascripts/pdf/index_spec.js61
-rw-r--r--spec/javascripts/pdf/page_spec.js57
-rw-r--r--spec/lib/constraints/group_url_constrainer_spec.rb7
-rw-r--r--spec/lib/gitlab/database/migration_helpers_spec.rb33
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb197
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb171
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb102
-rw-r--r--spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb54
-rw-r--r--spec/lib/gitlab/email/receiver_spec.rb10
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb14
-rw-r--r--spec/lib/gitlab/regex_spec.rb4
-rw-r--r--spec/lib/gitlab/request_profiler_spec.rb27
-rw-r--r--spec/lib/gitlab/user_access_spec.rb4
-rw-r--r--spec/models/concerns/cache_markdown_field_spec.rb6
-rw-r--r--spec/models/group_spec.rb26
-rw-r--r--spec/models/namespace_spec.rb8
-rw-r--r--spec/models/project_spec.rb43
-rw-r--r--spec/models/repository_spec.rb10
-rw-r--r--spec/models/user_spec.rb12
-rw-r--r--spec/requests/api/merge_requests_spec.rb13
-rw-r--r--spec/requests/api/v3/merge_requests_spec.rb13
-rw-r--r--spec/requests/request_profiler_spec.rb44
-rw-r--r--spec/routing/admin_routing_spec.rb14
-rw-r--r--spec/routing/project_routing_spec.rb6
-rw-r--r--spec/serializers/deployment_entity_spec.rb16
-rw-r--r--spec/serializers/status_entity_spec.rb6
-rw-r--r--spec/services/merge_requests/build_service_spec.rb10
-rw-r--r--spec/support/fake_migration_classes.rb3
-rw-r--r--spec/support/test_env.rb3
-rw-r--r--spec/tasks/gitlab/backup_rake_spec.rb6
-rw-r--r--spec/validators/dynamic_path_validator_spec.rb266
-rw-r--r--spec/workers/expire_build_instance_artifacts_worker_spec.rb6
-rw-r--r--yarn.lock26
217 files changed, 4682 insertions, 1010 deletions
diff --git a/.gitignore b/.gitignore
index dfc99a4ee48..bb818213de1 100644
--- a/.gitignore
+++ b/.gitignore
@@ -45,6 +45,7 @@ eslint-report.html
/public/uploads.*
/public/uploads/
/shared/artifacts/
+/spec/javascripts/fixtures/blob/pdf/
/rails_best_practices_output.html
/tags
/tmp/*
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index ddc2c5f2542..aa62a86d31d 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -253,38 +253,46 @@ spinach mysql 9 10: *spinach-knapsack-mysql
SETUP_DB: "false"
USE_BUNDLE_INSTALL: "true"
-.exec: &exec
+.rake-exec: &rake-exec
<<: *ruby-static-analysis
<<: *dedicated-runner
<<: *except-docs
stage: test
script:
- - bundle exec $CI_JOB_NAME
+ - bundle exec rake $CI_JOB_NAME
-rubocop:
+static-analysis:
<<: *ruby-static-analysis
<<: *dedicated-runner
- <<: *except-docs
stage: test
script:
- - bundle exec "rubocop --require rubocop-rspec"
-
-rake haml_lint: *exec
-rake scss_lint: *exec
-rake config_lint: *exec
-rake brakeman: *exec
-rake flay: *exec
-license_finder: *exec
-rake downtime_check:
- <<: *exec
+ - scripts/static-analysis
+
+docs:check:links:
+ image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
+ stage: test
+ <<: *dedicated-runner
+ cache: {}
+ dependencies: []
+ before_script: []
+ script:
+ - mv doc/ /nanoc/content/
+ - cd /nanoc
+ # Build HTML from Markdown
+ - bundle exec nanoc
+ # Check the internal links
+ - bundle exec nanoc check internal_links
+
+downtime_check:
+ <<: *rake-exec
except:
- master
- tags
- /^[\d-]+-stable(-ee)?$/
- /^docs\/*/
-rake ee_compat_check:
- <<: *exec
+ee_compat_check:
+ <<: *rake-exec
only:
- branches@gitlab-org/gitlab-ce
except:
@@ -306,22 +314,22 @@ rake ee_compat_check:
.db-migrate-reset: &db-migrate-reset
stage: test
<<: *dedicated-runner
+ <<: *except-docs
script:
- bundle exec rake db:migrate:reset
rake pg db:migrate:reset:
<<: *db-migrate-reset
<<: *use-pg
- <<: *except-docs
rake mysql db:migrate:reset:
<<: *db-migrate-reset
<<: *use-mysql
- <<: *except-docs
.db-rollback: &db-rollback
stage: test
<<: *dedicated-runner
+ <<: *except-docs
script:
- bundle exec rake db:rollback STEP=120
- bundle exec rake db:migrate
@@ -329,16 +337,15 @@ rake mysql db:migrate:reset:
rake pg db:rollback:
<<: *db-rollback
<<: *use-pg
- <<: *except-docs
rake mysql db:rollback:
<<: *db-rollback
<<: *use-mysql
- <<: *except-docs
.db-seed_fu: &db-seed_fu
stage: test
<<: *dedicated-runner
+ <<: *except-docs
variables:
SIZE: "1"
SETUP_DB: "false"
@@ -356,12 +363,10 @@ rake mysql db:rollback:
rake pg db:seed_fu:
<<: *db-seed_fu
<<: *use-pg
- <<: *except-docs
rake mysql db:seed_fu:
<<: *db-seed_fu
<<: *use-mysql
- <<: *except-docs
rake gitlab:assets:compile:
stage: test
@@ -402,31 +407,6 @@ rake karma:
paths:
- coverage-javascript/
-docs:check:apilint:
- image: "phusion/baseimage"
- stage: test
- <<: *dedicated-runner
- cache: {}
- dependencies: []
- before_script: []
- script:
- - scripts/lint-doc.sh
-
-docs:check:links:
- image: "registry.gitlab.com/gitlab-org/gitlab-build-images:nanoc-bootstrap-ruby-2.4-alpine"
- stage: test
- <<: *dedicated-runner
- cache: {}
- dependencies: []
- before_script: []
- script:
- - mv doc/ /nanoc/content/
- - cd /nanoc
- # Build HTML from Markdown
- - bundle exec nanoc
- # Check the internal links
- - bundle exec nanoc check internal_links
-
bundler:audit:
stage: test
<<: *ruby-static-analysis
@@ -485,14 +465,6 @@ coverage:
- coverage/index.html
- coverage/assets/
-lint:javascript:
- <<: *dedicated-runner
- <<: *except-docs
- stage: test
- before_script: []
- script:
- - yarn run eslint
-
lint:javascript:report:
<<: *dedicated-runner
<<: *except-docs
@@ -526,22 +498,6 @@ trigger_docs:
- master@gitlab-org/gitlab-ce
- master@gitlab-org/gitlab-ee
-# Notify slack in the end
-notify:slack:
- stage: post-test
- <<: *dedicated-runner
- variables:
- SETUP_DB: "false"
- USE_BUNDLE_INSTALL: "false"
- script:
- - ./scripts/notify_slack.sh "#development" "Build on \`$CI_COMMIT_REF_NAME\` failed! Commit \`$(git log -1 --oneline)\` See <https://gitlab.com/gitlab-org/$(basename "$PWD")/commit/"$CI_COMMIT_SHA"/pipelines>"
- when: on_failure
- only:
- - master@gitlab-org/gitlab-ce
- - tags@gitlab-org/gitlab-ce
- - master@gitlab-org/gitlab-ee
- - tags@gitlab-org/gitlab-ee
-
pages:
before_script: []
stage: pages
diff --git a/Gemfile b/Gemfile
index 41c2dcfd76e..f54a1f500fd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -85,14 +85,14 @@ gem 'kaminari', '~> 0.17.0'
gem 'hamlit', '~> 2.6.1'
# Files attachments
-gem 'carrierwave', '~> 0.11.0'
+gem 'carrierwave', '~> 1.0'
# Drag and Drop UI
gem 'dropzonejs-rails', '~> 0.7.1'
# for backups
gem 'fog-aws', '~> 0.9'
-gem 'fog-core', '~> 1.40'
+gem 'fog-core', '~> 1.44'
gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
diff --git a/Gemfile.lock b/Gemfile.lock
index 038d1f746b3..b822a325861 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -105,12 +105,10 @@ GEM
capybara-screenshot (1.0.14)
capybara (>= 1.0, < 3)
launchy
- carrierwave (0.11.2)
- activemodel (>= 3.2.0)
- activesupport (>= 3.2.0)
- json (>= 1.7)
+ carrierwave (1.0.0)
+ activemodel (>= 4.0.0)
+ activesupport (>= 4.0.0)
mime-types (>= 1.16)
- mimemagic (>= 0.3.0)
cause (0.1)
charlock_holmes (0.7.3)
chronic (0.10.2)
@@ -184,7 +182,7 @@ GEM
erubis (2.7.0)
escape_utils (1.1.1)
eventmachine (1.0.8)
- excon (0.52.0)
+ excon (0.55.0)
execjs (2.6.0)
expression_parser (0.9.0)
extlib (0.9.16)
@@ -210,12 +208,12 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
- fog-aws (0.11.0)
+ fog-aws (0.13.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
fog-xml (~> 0.1)
ipaddress (~> 0.8)
- fog-core (1.42.0)
+ fog-core (1.44.1)
builder
excon (~> 0.49)
formatador (~> 0.2)
@@ -237,9 +235,9 @@ GEM
fog-json (>= 1.0)
fog-xml (>= 0.1)
ipaddress (>= 0.8)
- fog-xml (0.1.2)
+ fog-xml (0.1.3)
fog-core
- nokogiri (~> 1.5, >= 1.5.11)
+ nokogiri (>= 1.5.11, < 2.0.0)
font-awesome-rails (4.7.0.1)
railties (>= 3.2, < 5.1)
foreman (0.78.0)
@@ -871,7 +869,7 @@ DEPENDENCIES
bundler-audit (~> 0.5.0)
capybara (~> 2.6.2)
capybara-screenshot (~> 1.0.0)
- carrierwave (~> 0.11.0)
+ carrierwave (~> 1.0)
charlock_holmes (~> 0.7.3)
chronic (~> 0.10.2)
chronic_duration (~> 0.10.6)
@@ -896,7 +894,7 @@ DEPENDENCIES
ffaker (~> 2.4)
flay (~> 2.8.0)
fog-aws (~> 0.9)
- fog-core (~> 1.40)
+ fog-core (~> 1.44)
fog-google (~> 0.5)
fog-local (~> 0.3)
fog-openstack (~> 0.1)
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
new file mode 100644
index 00000000000..4af3582b60d
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_created.ico b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
new file mode 100644
index 00000000000..13639da2e8a
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_failed.ico b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
new file mode 100644
index 00000000000..5f0e711b104
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_manual.ico b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
new file mode 100644
index 00000000000..8b1168a1267
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
new file mode 100644
index 00000000000..ed19b69e1c5
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_pending.ico b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
new file mode 100644
index 00000000000..5dfefd4cc5a
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_running.ico b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
new file mode 100644
index 00000000000..a41539c0e3e
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
new file mode 100644
index 00000000000..2c1ae552b93
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_success.ico b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
new file mode 100644
index 00000000000..70f0ca61eca
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/dev/favicon_status_warning.ico b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
new file mode 100644
index 00000000000..db289e03eb1
--- /dev/null
+++ b/app/assets/images/ci_favicons/dev/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_canceled.ico b/app/assets/images/ci_favicons/favicon_status_canceled.ico
index 5a19458f2a2..23adcffff50 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_canceled.ico
+++ b/app/assets/images/ci_favicons/favicon_status_canceled.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_created.ico b/app/assets/images/ci_favicons/favicon_status_created.ico
index 4dca9640cb3..f9d93b390d8 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_created.ico
+++ b/app/assets/images/ci_favicons/favicon_status_created.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_failed.ico b/app/assets/images/ci_favicons/favicon_status_failed.ico
index c961ff9a69b..28a22ebf724 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_failed.ico
+++ b/app/assets/images/ci_favicons/favicon_status_failed.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_manual.ico b/app/assets/images/ci_favicons/favicon_status_manual.ico
index 5fbbc99ea7c..dbbf1abf30c 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_manual.ico
+++ b/app/assets/images/ci_favicons/favicon_status_manual.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_not_found.ico b/app/assets/images/ci_favicons/favicon_status_not_found.ico
index 21afa9c72e6..49b9b232dd1 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_not_found.ico
+++ b/app/assets/images/ci_favicons/favicon_status_not_found.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_pending.ico b/app/assets/images/ci_favicons/favicon_status_pending.ico
index 8be32dab85a..05962f3f148 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_pending.ico
+++ b/app/assets/images/ci_favicons/favicon_status_pending.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_running.ico b/app/assets/images/ci_favicons/favicon_status_running.ico
index f328ff1a5ed..7fa3d4d48d4 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_running.ico
+++ b/app/assets/images/ci_favicons/favicon_status_running.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_skipped.ico b/app/assets/images/ci_favicons/favicon_status_skipped.ico
index b4394e1b4af..b0c26b62068 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_skipped.ico
+++ b/app/assets/images/ci_favicons/favicon_status_skipped.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_success.ico b/app/assets/images/ci_favicons/favicon_status_success.ico
index 4f436c95242..b150960b5be 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_success.ico
+++ b/app/assets/images/ci_favicons/favicon_status_success.ico
Binary files differ
diff --git a/app/assets/images/ci_favicons/favicon_status_warning.ico b/app/assets/images/ci_favicons/favicon_status_warning.ico
index 805cc20cdec..7e71d71684d 100755..100644
--- a/app/assets/images/ci_favicons/favicon_status_warning.ico
+++ b/app/assets/images/ci_favicons/favicon_status_warning.ico
Binary files differ
diff --git a/app/assets/javascripts/blob/pdf/index.js b/app/assets/javascripts/blob/pdf/index.js
index 9161be98853..0ed915c1ac9 100644
--- a/app/assets/javascripts/blob/pdf/index.js
+++ b/app/assets/javascripts/blob/pdf/index.js
@@ -1,11 +1,6 @@
/* eslint-disable no-new */
import Vue from 'vue';
-import PDFLab from 'vendor/pdflab';
-import workerSrc from 'vendor/pdf.worker';
-
-Vue.use(PDFLab, {
- workerSrc,
-});
+import pdfLab from '../../pdf/index.vue';
export default () => {
const el = document.getElementById('js-pdf-viewer');
@@ -20,6 +15,9 @@ export default () => {
pdf: el.dataset.endpoint,
};
},
+ components: {
+ pdfLab,
+ },
methods: {
onLoad() {
this.loading = false;
diff --git a/app/assets/javascripts/dispatcher.js b/app/assets/javascripts/dispatcher.js
index 15fe87f21ea..0bdce52cc89 100644
--- a/app/assets/javascripts/dispatcher.js
+++ b/app/assets/javascripts/dispatcher.js
@@ -44,6 +44,7 @@ import GroupsList from './groups_list';
import ProjectsList from './projects_list';
import MiniPipelineGraph from './mini_pipeline_graph_dropdown';
import BlobLinePermalinkUpdater from './blob/blob_line_permalink_updater';
+import Landing from './landing';
import BlobForkSuggestion from './blob/blob_fork_suggestion';
import UserCallout from './user_callout';
import { ProtectedTagCreate, ProtectedTagEditList } from './protected_tags';
@@ -148,8 +149,19 @@ const ShortcutsBlob = require('./shortcuts_blob');
new ProjectsList();
break;
case 'dashboard:groups:index':
+ new GroupsList();
+ break;
case 'explore:groups:index':
new GroupsList();
+
+ const landingElement = document.querySelector('.js-explore-groups-landing');
+ if (!landingElement) break;
+ const exploreGroupsLanding = new Landing(
+ landingElement,
+ landingElement.querySelector('.dismiss-button'),
+ 'explore_groups_landing_dismissed',
+ );
+ exploreGroupsLanding.toggle();
break;
case 'projects:milestones:new':
case 'projects:milestones:edit':
diff --git a/app/assets/javascripts/landing.js b/app/assets/javascripts/landing.js
new file mode 100644
index 00000000000..8c0950ad5d5
--- /dev/null
+++ b/app/assets/javascripts/landing.js
@@ -0,0 +1,37 @@
+import Cookies from 'js-cookie';
+
+class Landing {
+ constructor(landingElement, dismissButton, cookieName) {
+ this.landingElement = landingElement;
+ this.cookieName = cookieName;
+ this.dismissButton = dismissButton;
+ this.eventWrapper = {};
+ }
+
+ toggle() {
+ const isDismissed = this.isDismissed();
+
+ this.landingElement.classList.toggle('hidden', isDismissed);
+ if (!isDismissed) this.addEvents();
+ }
+
+ addEvents() {
+ this.eventWrapper.dismissLanding = this.dismissLanding.bind(this);
+ this.dismissButton.addEventListener('click', this.eventWrapper.dismissLanding);
+ }
+
+ removeEvents() {
+ this.dismissButton.removeEventListener('click', this.eventWrapper.dismissLanding);
+ }
+
+ dismissLanding() {
+ this.landingElement.classList.add('hidden');
+ Cookies.set(this.cookieName, 'true', { expires: 365 });
+ }
+
+ isDismissed() {
+ return Cookies.get(this.cookieName) === 'true';
+ }
+}
+
+export default Landing;
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
new file mode 100644
index 00000000000..c3a8da52404
--- /dev/null
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -0,0 +1,4 @@
+import d3 from 'd3';
+
+export const dateFormat = d3.time.format('%b %d, %Y');
+export const timeFormat = d3.time.format('%H:%M%p');
diff --git a/app/assets/javascripts/monitoring/deployments.js b/app/assets/javascripts/monitoring/deployments.js
new file mode 100644
index 00000000000..fc92ab61b31
--- /dev/null
+++ b/app/assets/javascripts/monitoring/deployments.js
@@ -0,0 +1,211 @@
+/* global Flash */
+import d3 from 'd3';
+import {
+ dateFormat,
+ timeFormat,
+} from './constants';
+
+export default class Deployments {
+ constructor(width, height) {
+ this.width = width;
+ this.height = height;
+
+ this.endpoint = document.getElementById('js-metrics').dataset.deploymentEndpoint;
+
+ this.createGradientDef();
+ }
+
+ init(chartData) {
+ this.chartData = chartData;
+
+ this.x = d3.time.scale().range([0, this.width]);
+ this.x.domain(d3.extent(this.chartData, d => d.time));
+
+ this.charts = d3.selectAll('.prometheus-graph');
+
+ this.getData();
+ }
+
+ getData() {
+ $.ajax({
+ url: this.endpoint,
+ dataType: 'JSON',
+ })
+ .fail(() => new Flash('Error getting deployment information.'))
+ .done((data) => {
+ this.data = data.deployments.reduce((deploymentDataArray, deployment) => {
+ const time = new Date(deployment.created_at);
+ const xPos = Math.floor(this.x(time));
+
+ time.setSeconds(this.chartData[0].time.getSeconds());
+
+ if (xPos >= 0) {
+ deploymentDataArray.push({
+ id: deployment.id,
+ time,
+ sha: deployment.sha,
+ tag: deployment.tag,
+ ref: deployment.ref.name,
+ xPos,
+ });
+ }
+
+ return deploymentDataArray;
+ }, []);
+
+ this.plotData();
+ });
+ }
+
+ plotData() {
+ this.charts.each((d, i) => {
+ const svg = d3.select(this.charts[0][i]);
+ const chart = svg.select('.graph-container');
+ const key = svg.node().getAttribute('graph-type');
+
+ this.createLine(chart, key);
+ this.createDeployInfoBox(chart, key);
+ });
+ }
+
+ createGradientDef() {
+ const defs = d3.select('body')
+ .append('svg')
+ .attr({
+ height: 0,
+ width: 0,
+ })
+ .append('defs');
+
+ defs.append('linearGradient')
+ .attr({
+ id: 'shadow-gradient',
+ })
+ .append('stop')
+ .attr({
+ offset: '0%',
+ 'stop-color': '#000',
+ 'stop-opacity': 0.4,
+ })
+ .select(this.selectParentNode)
+ .append('stop')
+ .attr({
+ offset: '100%',
+ 'stop-color': '#000',
+ 'stop-opacity': 0,
+ });
+ }
+
+ createLine(chart, key) {
+ chart.append('g')
+ .attr({
+ class: 'deploy-info',
+ })
+ .selectAll('.deploy-info')
+ .data(this.data)
+ .enter()
+ .append('g')
+ .attr({
+ class: d => `deploy-info-${d.id}-${key}`,
+ transform: d => `translate(${Math.floor(d.xPos) + 1}, 0)`,
+ })
+ .append('rect')
+ .attr({
+ x: 1,
+ y: 0,
+ height: this.height + 1,
+ width: 3,
+ fill: 'url(#shadow-gradient)',
+ })
+ .select(this.selectParentNode)
+ .append('line')
+ .attr({
+ class: 'deployment-line',
+ x1: 0,
+ x2: 0,
+ y1: 0,
+ y2: this.height + 1,
+ });
+ }
+
+ createDeployInfoBox(chart, key) {
+ chart.selectAll('.deploy-info')
+ .selectAll('.js-deploy-info-box')
+ .data(this.data)
+ .enter()
+ .select(d => document.querySelector(`.deploy-info-${d.id}-${key}`))
+ .append('svg')
+ .attr({
+ class: 'js-deploy-info-box hidden',
+ x: 3,
+ y: 0,
+ width: 92,
+ height: 60,
+ })
+ .append('rect')
+ .attr({
+ class: 'rect-text-metric deploy-info-rect rect-metric',
+ x: 1,
+ y: 1,
+ rx: 2,
+ width: 90,
+ height: 58,
+ })
+ .select(this.selectParentNode)
+ .append('g')
+ .attr({
+ transform: 'translate(5, 2)',
+ })
+ .append('text')
+ .attr({
+ class: 'deploy-info-text text-metric-bold',
+ })
+ .text(Deployments.refText)
+ .select(this.selectParentNode)
+ .append('text')
+ .attr({
+ class: 'deploy-info-text',
+ y: 18,
+ })
+ .text(d => dateFormat(d.time))
+ .select(this.selectParentNode)
+ .append('text')
+ .attr({
+ class: 'deploy-info-text text-metric-bold',
+ y: 38,
+ })
+ .text(d => timeFormat(d.time));
+ }
+
+ static toggleDeployTextbox(deploy, key, showInfoBox) {
+ d3.selectAll(`.deploy-info-${deploy.id}-${key} .js-deploy-info-box`)
+ .classed('hidden', !showInfoBox);
+ }
+
+ mouseOverDeployInfo(mouseXPos, key) {
+ if (!this.data) return false;
+
+ let dataFound = false;
+
+ this.data.forEach((d) => {
+ if (d.xPos >= mouseXPos - 10 && d.xPos <= mouseXPos + 10 && !dataFound) {
+ dataFound = d.xPos + 1;
+
+ Deployments.toggleDeployTextbox(d, key, true);
+ } else {
+ Deployments.toggleDeployTextbox(d, key, false);
+ }
+ });
+
+ return dataFound;
+ }
+
+ /* `this` is bound to the D3 node */
+ selectParentNode() {
+ return this.parentNode;
+ }
+
+ static refText(d) {
+ return d.tag ? d.ref : d.sha.slice(0, 6);
+ }
+}
diff --git a/app/assets/javascripts/monitoring/prometheus_graph.js b/app/assets/javascripts/monitoring/prometheus_graph.js
index 78bb0e6fb47..6af88769129 100644
--- a/app/assets/javascripts/monitoring/prometheus_graph.js
+++ b/app/assets/javascripts/monitoring/prometheus_graph.js
@@ -3,16 +3,20 @@
import d3 from 'd3';
import statusCodes from '~/lib/utils/http_status';
-import { formatRelevantDigits } from '~/lib/utils/number_utils';
+import Deployments from './deployments';
+import '../lib/utils/common_utils';
+import { formatRelevantDigits } from '../lib/utils/number_utils';
import '../flash';
+import {
+ dateFormat,
+ timeFormat,
+} from './constants';
const prometheusContainer = '.prometheus-container';
const prometheusParentGraphContainer = '.prometheus-graphs';
const prometheusGraphsContainer = '.prometheus-graph';
const prometheusStatesContainer = '.prometheus-state';
const metricsEndpoint = 'metrics.json';
-const timeFormat = d3.time.format('%H:%M');
-const dayFormat = d3.time.format('%b %e, %a');
const bisectDate = d3.bisector(d => d.time).left;
const extraAddedWidthParent = 100;
@@ -36,6 +40,7 @@ class PrometheusGraph {
this.width = parentContainerWidth - this.margin.left - this.margin.right;
this.height = this.originalHeight - this.margin.top - this.margin.bottom;
this.backOffRequestCounter = 0;
+ this.deployments = new Deployments(this.width, this.height);
this.configureGraph();
this.init();
} else {
@@ -74,6 +79,12 @@ class PrometheusGraph {
$(prometheusParentGraphContainer).show();
this.transformData(metricsResponse);
this.createGraph();
+
+ const firstMetricData = this.graphSpecificProperties[
+ Object.keys(this.graphSpecificProperties)[0]
+ ].data;
+
+ this.deployments.init(firstMetricData);
}
});
}
@@ -96,6 +107,7 @@ class PrometheusGraph {
.attr('width', this.width + this.margin.left + this.margin.right)
.attr('height', this.height + this.margin.bottom + this.margin.top)
.append('g')
+ .attr('class', 'graph-container')
.attr('transform', `translate(${this.margin.left},${this.margin.top})`);
const axisLabelContainer = d3.select(prometheusGraphContainer)
@@ -116,6 +128,7 @@ class PrometheusGraph {
.scale(y)
.ticks(this.commonGraphProperties.axis_no_ticks)
.tickSize(-this.width)
+ .outerTickSize(0)
.orient('left');
this.createAxisLabelContainers(axisLabelContainer, key);
@@ -248,7 +261,8 @@ class PrometheusGraph {
const d1 = currentGraphProps.data[overlayIndex];
const evalTime = timeValueOverlay - d0.time > d1.time - timeValueOverlay;
const currentData = evalTime ? d1 : d0;
- const currentTimeCoordinate = currentGraphProps.xScale(currentData.time);
+ const currentTimeCoordinate = Math.floor(currentGraphProps.xScale(currentData.time));
+ const currentDeployXPos = this.deployments.mouseOverDeployInfo(currentXCoordinate, key);
const currentPrometheusGraphContainer = `${prometheusGraphsContainer}[graph-type=${key}]`;
const maxValueFromData = d3.max(currentGraphProps.data.map(metricValue => metricValue.value));
const maxMetricValue = currentGraphProps.yScale(maxValueFromData);
@@ -256,13 +270,12 @@ class PrometheusGraph {
// Clear up all the pieces of the flag
d3.selectAll(`${currentPrometheusGraphContainer} .selected-metric-line`).remove();
d3.selectAll(`${currentPrometheusGraphContainer} .circle-metric`).remove();
- d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric`).remove();
- d3.selectAll(`${currentPrometheusGraphContainer} .text-metric`).remove();
+ d3.selectAll(`${currentPrometheusGraphContainer} .rect-text-metric:not(.deploy-info-rect)`).remove();
const currentChart = d3.select(currentPrometheusGraphContainer).select('g');
currentChart.append('line')
- .attr('class', 'selected-metric-line')
.attr({
+ class: `${currentDeployXPos ? 'hidden' : ''} selected-metric-line`,
x1: currentTimeCoordinate,
y1: currentGraphProps.yScale(0),
x2: currentTimeCoordinate,
@@ -272,33 +285,45 @@ class PrometheusGraph {
currentChart.append('circle')
.attr('class', 'circle-metric')
.attr('fill', currentGraphProps.line_color)
- .attr('cx', currentTimeCoordinate)
+ .attr('cx', currentDeployXPos || currentTimeCoordinate)
.attr('cy', currentGraphProps.yScale(currentData.value))
.attr('r', this.commonGraphProperties.circle_radius_metric);
+ if (currentDeployXPos) return;
+
// The little box with text
- const rectTextMetric = currentChart.append('g')
- .attr('class', 'rect-text-metric')
- .attr('translate', `(${currentTimeCoordinate}, ${currentGraphProps.yScale(currentData.value)})`);
+ const rectTextMetric = currentChart.append('svg')
+ .attr({
+ class: 'rect-text-metric',
+ x: currentTimeCoordinate,
+ y: 0,
+ });
rectTextMetric.append('rect')
- .attr('class', 'rect-metric')
- .attr('x', currentTimeCoordinate + 10)
- .attr('y', maxMetricValue)
- .attr('width', this.commonGraphProperties.rect_text_width)
- .attr('height', this.commonGraphProperties.rect_text_height);
+ .attr({
+ class: 'rect-metric',
+ x: 4,
+ y: 1,
+ rx: 2,
+ width: this.commonGraphProperties.rect_text_width,
+ height: this.commonGraphProperties.rect_text_height,
+ });
rectTextMetric.append('text')
- .attr('class', 'text-metric')
- .attr('x', currentTimeCoordinate + 35)
- .attr('y', maxMetricValue + 35)
+ .attr({
+ class: 'text-metric text-metric-bold',
+ x: 8,
+ y: 35,
+ })
.text(timeFormat(currentData.time));
rectTextMetric.append('text')
- .attr('class', 'text-metric-date')
- .attr('x', currentTimeCoordinate + 15)
- .attr('y', maxMetricValue + 15)
- .text(dayFormat(currentData.time));
+ .attr({
+ class: 'text-metric-date',
+ x: 8,
+ y: 15,
+ })
+ .text(dateFormat(currentData.time));
let currentMetricValue = formatRelevantDigits(currentData.value);
if (key === 'cpu_values') {
diff --git a/app/assets/javascripts/pdf/assets/img/bg.gif b/app/assets/javascripts/pdf/assets/img/bg.gif
new file mode 100644
index 00000000000..c7e98e044f5
--- /dev/null
+++ b/app/assets/javascripts/pdf/assets/img/bg.gif
Binary files differ
diff --git a/app/assets/javascripts/pdf/index.vue b/app/assets/javascripts/pdf/index.vue
new file mode 100644
index 00000000000..4603859d7b0
--- /dev/null
+++ b/app/assets/javascripts/pdf/index.vue
@@ -0,0 +1,73 @@
+<template>
+ <div class="pdf-viewer" v-if="hasPDF">
+ <page v-for="(page, index) in pages"
+ :key="index"
+ :v-if="!loading"
+ :page="page"
+ :number="index + 1" />
+ </div>
+</template>
+
+<script>
+ import pdfjsLib from 'pdfjs-dist';
+ import workerSrc from 'vendor/pdf.worker';
+
+ import page from './page/index.vue';
+
+ export default {
+ props: {
+ pdf: {
+ type: [String, Uint8Array],
+ required: true,
+ },
+ },
+ data() {
+ return {
+ loading: false,
+ pages: [],
+ };
+ },
+ components: { page },
+ watch: { pdf: 'load' },
+ computed: {
+ document() {
+ return typeof this.pdf === 'string' ? this.pdf : { data: this.pdf };
+ },
+ hasPDF() {
+ return this.pdf && this.pdf.length > 0;
+ },
+ },
+ methods: {
+ load() {
+ this.pages = [];
+ return pdfjsLib.getDocument(this.document)
+ .then(this.renderPages)
+ .then(() => this.$emit('pdflabload'))
+ .catch(error => this.$emit('pdflaberror', error))
+ .then(() => { this.loading = false; });
+ },
+ renderPages(pdf) {
+ const pagePromises = [];
+ this.loading = true;
+ for (let num = 1; num <= pdf.numPages; num += 1) {
+ pagePromises.push(
+ pdf.getPage(num).then(p => this.pages.push(p)),
+ );
+ }
+ return Promise.all(pagePromises);
+ },
+ },
+ mounted() {
+ pdfjsLib.PDFJS.workerSrc = workerSrc;
+ if (this.hasPDF) this.load();
+ },
+ };
+</script>
+
+<style>
+ .pdf-viewer {
+ background: url('./assets/img/bg.gif');
+ display: flex;
+ flex-flow: column nowrap;
+ }
+</style>
diff --git a/app/assets/javascripts/pdf/page/index.vue b/app/assets/javascripts/pdf/page/index.vue
new file mode 100644
index 00000000000..7b74ee4eb2e
--- /dev/null
+++ b/app/assets/javascripts/pdf/page/index.vue
@@ -0,0 +1,68 @@
+<template>
+ <canvas
+ class="pdf-page"
+ ref="canvas"
+ :data-page="number" />
+</template>
+
+<script>
+ export default {
+ props: {
+ page: {
+ type: Object,
+ required: true,
+ },
+ number: {
+ type: Number,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ scale: 4,
+ rendering: false,
+ };
+ },
+ computed: {
+ viewport() {
+ return this.page.getViewport(this.scale);
+ },
+ context() {
+ return this.$refs.canvas.getContext('2d');
+ },
+ renderContext() {
+ return {
+ canvasContext: this.context,
+ viewport: this.viewport,
+ };
+ },
+ },
+ mounted() {
+ this.$refs.canvas.height = this.viewport.height;
+ this.$refs.canvas.width = this.viewport.width;
+ this.rendering = true;
+ this.page.render(this.renderContext)
+ .then(() => { this.rendering = false; })
+ .catch(error => this.$emit('pdflaberror', error));
+ },
+ };
+</script>
+
+<style>
+.pdf-page {
+ margin: 8px auto 0 auto;
+ border-top: 1px #ddd solid;
+ border-bottom: 1px #ddd solid;
+ width: 100%;
+}
+
+.pdf-page:first-child {
+ margin-top: 0px;
+ border-top: 0px;
+}
+
+.pdf-page:last-child {
+ margin-bottom: 0px;
+ border-bottom: 0px;
+}
+</style>
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index f3e2a5db0a6..ac1fc0eb8ae 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -254,6 +254,63 @@
padding: 10px 0;
}
+.landing {
+ margin-bottom: $gl-padding;
+ overflow: hidden;
+ display: flex;
+ position: relative;
+ border: 1px solid $blue-300;
+ border-radius: $border-radius-default;
+ background-color: $blue-25;
+ justify-content: center;
+
+ .dismiss-button {
+ position: absolute;
+ right: 6px;
+ top: 6px;
+ cursor: pointer;
+ color: $blue-300;
+ z-index: 1;
+ border: none;
+ background-color: transparent;
+
+ &:hover,
+ &:focus {
+ border: none;
+ color: $blue-400;
+ }
+ }
+
+ .svg-container {
+ align-self: center;
+ }
+
+ .inner-content {
+ text-align: left;
+ white-space: nowrap;
+
+ h4 {
+ color: $gl-text-color;
+ font-size: 17px;
+ }
+
+ p {
+ color: $gl-text-color;
+ margin-bottom: $gl-padding;
+ }
+ }
+
+ @media (max-width: $screen-sm-min) {
+ flex-direction: column;
+
+ .inner-content {
+ white-space: normal;
+ padding: 0 28px;
+ text-align: center;
+ }
+ }
+}
+
.empty-state {
margin: 100px 0 0;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 638c1403b25..1a6f36d032d 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -424,6 +424,11 @@ table {
}
}
+.bordered-box {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
.str-truncated {
&-60 {
@include str-truncated(60%);
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index ad3dbc7ac48..403724cd68a 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -93,11 +93,6 @@
top: $gl-padding-top;
}
- .bordered-box {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- }
-
.content-list {
li {
padding: 18px $gl-padding $gl-padding;
@@ -139,42 +134,9 @@
}
}
- .landing {
- margin-bottom: $gl-padding;
- overflow: hidden;
-
- .dismiss-icon {
- position: absolute;
- right: $cycle-analytics-box-padding;
- cursor: pointer;
- color: $cycle-analytics-dismiss-icon-color;
- }
-
- .svg-container {
- text-align: center;
-
- svg {
- width: 136px;
- height: 136px;
- }
- }
-
- .inner-content {
- @media (max-width: $screen-xs-max) {
- padding: 0 28px;
- text-align: center;
- }
-
- h4 {
- color: $gl-text-color;
- font-size: 17px;
- }
-
- p {
- color: $cycle-analytics-box-text-color;
- margin-bottom: $gl-padding;
- }
- }
+ .landing svg {
+ width: 136px;
+ height: 136px;
}
.fa-spinner {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 72e7d42858d..026d35295d7 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -157,7 +157,8 @@
.prometheus-graph {
text {
- fill: $stat-graph-axis-fill;
+ fill: $gl-text-color;
+ stroke-width: 0;
}
.label-axis-text,
@@ -210,27 +211,33 @@
.rect-text-metric {
fill: $white-light;
stroke-width: 1;
- stroke: $black;
+ stroke: $gray-darkest;
}
.rect-axis-text {
fill: $white-light;
}
-.text-metric,
-.text-median-metric,
-.text-metric-usage,
-.text-metric-date {
- fill: $black;
+.text-metric {
+ font-weight: 600;
}
-.text-metric-date {
- font-weight: 200;
+.selected-metric-line {
+ stroke: $gl-gray-dark;
+ stroke-width: 1;
}
-.selected-metric-line {
+.deployment-line {
stroke: $black;
- stroke-width: 1;
+ stroke-width: 2;
+}
+
+.deploy-info-text {
+ dominant-baseline: text-before-edge;
+}
+
+.text-metric-bold {
+ font-weight: 600;
}
.prometheus-state {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index 73a5889867a..72d73b89a2a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -88,3 +88,26 @@
color: $gl-text-color-secondary;
margin-top: 10px;
}
+
+.explore-groups.landing {
+ margin-top: 10px;
+
+ .inner-content {
+ padding: 0;
+
+ p {
+ margin: 7px 0 0;
+ max-width: 480px;
+ padding: 0 $gl-padding;
+
+ @media (max-width: $screen-sm-min) {
+ margin: 0 auto;
+ }
+ }
+ }
+
+ svg {
+ width: 62px;
+ height: 50px;
+ }
+}
diff --git a/app/controllers/admin/hooks_controller.rb b/app/controllers/admin/hooks_controller.rb
index cbfc4581411..a119934febc 100644
--- a/app/controllers/admin/hooks_controller.rb
+++ b/app/controllers/admin/hooks_controller.rb
@@ -1,4 +1,6 @@
class Admin::HooksController < Admin::ApplicationController
+ before_action :hook, only: :edit
+
def index
@hooks = SystemHook.all
@hook = SystemHook.new
@@ -15,15 +17,25 @@ class Admin::HooksController < Admin::ApplicationController
end
end
+ def edit
+ end
+
+ def update
+ if hook.update_attributes(hook_params)
+ flash[:notice] = 'System hook was successfully updated.'
+ redirect_to admin_hooks_path
+ else
+ render 'edit'
+ end
+ end
+
def destroy
- @hook = SystemHook.find(params[:id])
- @hook.destroy
+ hook.destroy
redirect_to admin_hooks_path
end
def test
- @hook = SystemHook.find(params[:hook_id])
data = {
event_name: "project_create",
name: "Ruby",
@@ -32,11 +44,17 @@ class Admin::HooksController < Admin::ApplicationController
owner_name: "Someone",
owner_email: "example@gitlabhq.com"
}
- @hook.execute(data, 'system_hooks')
+ hook.execute(data, 'system_hooks')
redirect_back_or_default
end
+ private
+
+ def hook
+ @hook ||= SystemHook.find(params[:id])
+ end
+
def hook_params
params.require(:hook).permit(
:enable_ssl_verification,
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
new file mode 100644
index 00000000000..c32038d07bf
--- /dev/null
+++ b/app/controllers/concerns/notes_actions.rb
@@ -0,0 +1,136 @@
+module NotesActions
+ include RendersNotes
+ extend ActiveSupport::Concern
+
+ included do
+ before_action :authorize_admin_note!, only: [:update, :destroy]
+ end
+
+ def index
+ current_fetched_at = Time.now.to_i
+
+ notes_json = { notes: [], last_fetched_at: current_fetched_at }
+
+ @notes = notes_finder.execute.inc_relations_for_view
+ @notes = prepare_notes_for_rendering(@notes)
+
+ @notes.each do |note|
+ next if note.cross_reference_not_visible_for?(current_user)
+
+ notes_json[:notes] << note_json(note)
+ end
+
+ render json: notes_json
+ end
+
+ def create
+ create_params = note_params.merge(
+ merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
+ in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
+ )
+ @note = Notes::CreateService.new(project, current_user, create_params).execute
+
+ if @note.is_a?(Note)
+ Banzai::NoteRenderer.render([@note], @project, current_user)
+ end
+
+ respond_to do |format|
+ format.json { render json: note_json(@note) }
+ format.html { redirect_back_or_default }
+ end
+ end
+
+ def update
+ @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
+
+ if @note.is_a?(Note)
+ Banzai::NoteRenderer.render([@note], @project, current_user)
+ end
+
+ respond_to do |format|
+ format.json { render json: note_json(@note) }
+ format.html { redirect_back_or_default }
+ end
+ end
+
+ def destroy
+ if note.editable?
+ Notes::DestroyService.new(project, current_user).execute(note)
+ end
+
+ respond_to do |format|
+ format.js { head :ok }
+ end
+ end
+
+ private
+
+ def note_json(note)
+ attrs = {
+ commands_changes: note.commands_changes
+ }
+
+ if note.persisted?
+ attrs.merge!(
+ valid: true,
+ id: note.id,
+ discussion_id: note.discussion_id(noteable),
+ html: note_html(note),
+ note: note.note
+ )
+
+ discussion = note.to_discussion(noteable)
+ unless discussion.individual_note?
+ attrs.merge!(
+ discussion_resolvable: discussion.resolvable?,
+
+ diff_discussion_html: diff_discussion_html(discussion),
+ discussion_html: discussion_html(discussion)
+ )
+ end
+ else
+ attrs.merge!(
+ valid: false,
+ errors: note.errors
+ )
+ end
+
+ attrs
+ end
+
+ def authorize_admin_note!
+ return access_denied! unless can?(current_user, :admin_note, note)
+ end
+
+ def note_params
+ params.require(:note).permit(
+ :project_id,
+ :noteable_type,
+ :noteable_id,
+ :commit_id,
+ :noteable,
+ :type,
+
+ :note,
+ :attachment,
+
+ # LegacyDiffNote
+ :line_code,
+
+ # DiffNote
+ :position
+ )
+ end
+
+ def noteable
+ @noteable ||= notes_finder.target
+ end
+
+ def last_fetched_at
+ request.headers['X-Last-Fetched-At']
+ end
+
+ def notes_finder
+ @notes_finder ||= NotesFinder.new(project, current_user, finder_params)
+ end
+end
diff --git a/app/controllers/concerns/renders_notes.rb b/app/controllers/concerns/renders_notes.rb
index dd21066ac13..41c3114ad1e 100644
--- a/app/controllers/concerns/renders_notes.rb
+++ b/app/controllers/concerns/renders_notes.rb
@@ -10,6 +10,8 @@ module RendersNotes
private
def preload_max_access_for_authors(notes, project)
+ return nil unless project
+
user_ids = notes.map(&:author_id)
project.team.max_member_access_for_user_ids(user_ids)
end
diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb
index ca6dffe1cc5..ffea712a833 100644
--- a/app/controllers/concerns/snippets_actions.rb
+++ b/app/controllers/concerns/snippets_actions.rb
@@ -5,10 +5,12 @@ module SnippetsActions
end
def raw
+ disposition = params[:inline] == 'false' ? 'attachment' : 'inline'
+
send_data(
convert_line_endings(@snippet.content),
type: 'text/plain; charset=utf-8',
- disposition: 'inline',
+ disposition: disposition,
filename: @snippet.sanitized_file_name
)
end
diff --git a/app/controllers/concerns/toggle_award_emoji.rb b/app/controllers/concerns/toggle_award_emoji.rb
index fbf9a026b10..ba5b7d33f87 100644
--- a/app/controllers/concerns/toggle_award_emoji.rb
+++ b/app/controllers/concerns/toggle_award_emoji.rb
@@ -22,7 +22,8 @@ module ToggleAwardEmoji
def to_todoable(awardable)
case awardable
when Note
- awardable.noteable
+ # we don't create todos for personal snippet comments for now
+ awardable.for_personal_snippet? ? nil : awardable.noteable
when MergeRequest, Issue
awardable
when Snippet
diff --git a/app/controllers/projects/deployments_controller.rb b/app/controllers/projects/deployments_controller.rb
new file mode 100644
index 00000000000..c319671456d
--- /dev/null
+++ b/app/controllers/projects/deployments_controller.rb
@@ -0,0 +1,18 @@
+class Projects::DeploymentsController < Projects::ApplicationController
+ before_action :authorize_read_environment!
+ before_action :authorize_read_deployment!
+
+ def index
+ deployments = environment.deployments.reorder(created_at: :desc)
+ deployments = deployments.where('created_at > ?', params[:after].to_time) if params[:after]&.to_time
+
+ render json: { deployments: DeploymentSerializer.new(user: @current_user, project: project)
+ .represent_concise(deployments) }
+ end
+
+ private
+
+ def environment
+ @environment ||= project.environments.find(params[:environment_id])
+ end
+end
diff --git a/app/controllers/projects/hooks_controller.rb b/app/controllers/projects/hooks_controller.rb
index 1e41f980f31..86d13a0d222 100644
--- a/app/controllers/projects/hooks_controller.rb
+++ b/app/controllers/projects/hooks_controller.rb
@@ -1,6 +1,7 @@
class Projects::HooksController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
+ before_action :hook, only: :edit
respond_to :html
@@ -17,6 +18,18 @@ class Projects::HooksController < Projects::ApplicationController
redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
end
+ def edit
+ end
+
+ def update
+ if hook.update_attributes(hook_params)
+ flash[:notice] = 'Hook was successfully updated.'
+ redirect_to namespace_project_settings_integrations_path(@project.namespace, @project)
+ else
+ render 'edit'
+ end
+ end
+
def test
if !@project.empty_repo?
status, message = TestHookService.new.execute(hook, current_user)
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 405ea3c0a4f..37f51b2ebe3 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -1,68 +1,22 @@
class Projects::NotesController < Projects::ApplicationController
- include RendersNotes
+ include NotesActions
include ToggleAwardEmoji
- # Authorize
before_action :authorize_read_note!
before_action :authorize_create_note!, only: [:create]
- before_action :authorize_admin_note!, only: [:update, :destroy]
before_action :authorize_resolve_note!, only: [:resolve, :unresolve]
- def index
- current_fetched_at = Time.now.to_i
-
- notes_json = { notes: [], last_fetched_at: current_fetched_at }
-
- @notes = notes_finder.execute.inc_relations_for_view
- @notes = prepare_notes_for_rendering(@notes)
-
- @notes.each do |note|
- next if note.cross_reference_not_visible_for?(current_user)
-
- notes_json[:notes] << note_json(note)
- end
-
- render json: notes_json
- end
-
+ #
+ # This is a fix to make spinach feature tests passing:
+ # Controller actions are returned from AbstractController::Base and methods of parent classes are
+ # excluded in order to return only specific controller related methods.
+ # That is ok for the app (no :create method in ancestors)
+ # but fails for tests because there is a :create method on FactoryGirl (one of the ancestors)
+ #
+ # see https://github.com/rails/rails/blob/v4.2.7/actionpack/lib/abstract_controller/base.rb#L78
+ #
def create
- create_params = note_params.merge(
- merge_request_diff_head_sha: params[:merge_request_diff_head_sha],
- in_reply_to_discussion_id: params[:in_reply_to_discussion_id]
- )
- @note = Notes::CreateService.new(project, current_user, create_params).execute
-
- if @note.is_a?(Note)
- Banzai::NoteRenderer.render([@note], @project, current_user)
- end
-
- respond_to do |format|
- format.json { render json: note_json(@note) }
- format.html { redirect_back_or_default }
- end
- end
-
- def update
- @note = Notes::UpdateService.new(project, current_user, note_params).execute(note)
-
- if @note.is_a?(Note)
- Banzai::NoteRenderer.render([@note], @project, current_user)
- end
-
- respond_to do |format|
- format.json { render json: note_json(@note) }
- format.html { redirect_back_or_default }
- end
- end
-
- def destroy
- if note.editable?
- Notes::DestroyService.new(project, current_user).execute(note)
- end
-
- respond_to do |format|
- format.js { head :ok }
- end
+ super
end
def delete_attachment
@@ -110,7 +64,7 @@ class Projects::NotesController < Projects::ApplicationController
def note_html(note)
render_to_string(
- "projects/notes/_note",
+ "shared/notes/_note",
layout: false,
formats: [:html],
locals: { note: note }
@@ -152,76 +106,11 @@ class Projects::NotesController < Projects::ApplicationController
)
end
- def note_json(note)
- attrs = {
- commands_changes: note.commands_changes
- }
-
- if note.persisted?
- attrs.merge!(
- valid: true,
- id: note.id,
- discussion_id: note.discussion_id(noteable),
- html: note_html(note),
- note: note.note
- )
-
- discussion = note.to_discussion(noteable)
- unless discussion.individual_note?
- attrs.merge!(
- discussion_resolvable: discussion.resolvable?,
-
- diff_discussion_html: diff_discussion_html(discussion),
- discussion_html: discussion_html(discussion)
- )
- end
- else
- attrs.merge!(
- valid: false,
- errors: note.errors
- )
- end
-
- attrs
- end
-
- def authorize_admin_note!
- return access_denied! unless can?(current_user, :admin_note, note)
+ def finder_params
+ params.merge(last_fetched_at: last_fetched_at)
end
def authorize_resolve_note!
return access_denied! unless can?(current_user, :resolve_note, note)
end
-
- def note_params
- params.require(:note).permit(
- :project_id,
- :noteable_type,
- :noteable_id,
- :commit_id,
- :noteable,
- :type,
-
- :note,
- :attachment,
-
- # LegacyDiffNote
- :line_code,
-
- # DiffNote
- :position
- )
- end
-
- def notes_finder
- @notes_finder ||= NotesFinder.new(project, current_user, params.merge(last_fetched_at: last_fetched_at))
- end
-
- def noteable
- @noteable ||= notes_finder.target
- end
-
- def last_fetched_at
- request.headers['X-Last-Fetched-At']
- end
end
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
new file mode 100644
index 00000000000..3c4ddc1680d
--- /dev/null
+++ b/app/controllers/snippets/notes_controller.rb
@@ -0,0 +1,44 @@
+class Snippets::NotesController < ApplicationController
+ include NotesActions
+ include ToggleAwardEmoji
+
+ skip_before_action :authenticate_user!, only: [:index]
+ before_action :snippet
+ before_action :authorize_read_snippet!, only: [:show, :index, :create]
+
+ private
+
+ def note
+ @note ||= snippet.notes.find(params[:id])
+ end
+ alias_method :awardable, :note
+
+ def note_html(note)
+ render_to_string(
+ "shared/notes/_note",
+ layout: false,
+ formats: [:html],
+ locals: { note: note }
+ )
+ end
+
+ def project
+ nil
+ end
+
+ def snippet
+ PersonalSnippet.find_by(id: params[:snippet_id])
+ end
+
+ def note_params
+ super.merge(noteable_id: params[:snippet_id])
+ end
+
+ def finder_params
+ params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
+ end
+
+ def authorize_read_snippet!
+ return render_404 unless can?(current_user, :read_personal_snippet, snippet)
+ end
+end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 906833505d1..da1ae9a34d9 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -1,14 +1,15 @@
class SnippetsController < ApplicationController
+ include RendersNotes
include ToggleAwardEmoji
include SpammableActions
include SnippetsActions
include MarkdownPreview
include RendersBlob
- before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :download]
+ before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
# Allow read snippet
- before_action :authorize_read_snippet!, only: [:show, :raw, :download]
+ before_action :authorize_read_snippet!, only: [:show, :raw]
# Allow modify snippet
before_action :authorize_update_snippet!, only: [:edit, :update]
@@ -16,7 +17,7 @@ class SnippetsController < ApplicationController
# Allow destroy snippet
before_action :authorize_admin_snippet!, only: [:destroy]
- skip_before_action :authenticate_user!, only: [:index, :show, :raw, :download]
+ skip_before_action :authenticate_user!, only: [:index, :show, :raw]
layout 'snippets'
respond_to :html
@@ -64,6 +65,11 @@ class SnippetsController < ApplicationController
blob = @snippet.blob
override_max_blob_size(blob)
+ @noteable = @snippet
+
+ @discussions = @snippet.discussions
+ @notes = prepare_notes_for_rendering(@discussions.flat_map(&:notes))
+
respond_to do |format|
format.html do
render 'show'
@@ -83,14 +89,6 @@ class SnippetsController < ApplicationController
redirect_to snippets_path
end
- def download
- send_data(
- convert_line_endings(@snippet.content),
- type: 'text/plain; charset=utf-8',
- filename: @snippet.sanitized_file_name
- )
- end
-
def preview_markdown
render_markdown_preview(params[:text], skip_project_check: true)
end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 3c499184b41..dc6a8ad1f66 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -68,6 +68,8 @@ class NotesFinder
MergeRequestsFinder.new(@current_user, project_id: @project.id).execute
when "snippet", "project_snippet"
SnippetsFinder.new.execute(@current_user, filter: :by_project, project: @project)
+ when "personal_snippet"
+ PersonalSnippet.all
else
raise 'invalid target_type'
end
diff --git a/app/helpers/award_emoji_helper.rb b/app/helpers/award_emoji_helper.rb
index 167b09e678f..024cf38469e 100644
--- a/app/helpers/award_emoji_helper.rb
+++ b/app/helpers/award_emoji_helper.rb
@@ -1,10 +1,14 @@
module AwardEmojiHelper
def toggle_award_url(awardable)
- return url_for([:toggle_award_emoji, awardable]) unless @project
+ return url_for([:toggle_award_emoji, awardable]) unless @project || awardable.is_a?(Note)
if awardable.is_a?(Note)
# We render a list of notes very frequently and calling the specific method is a lot faster than the generic one (4.5x)
- toggle_award_emoji_namespace_project_note_url(@project.namespace, @project, awardable.id)
+ if awardable.for_personal_snippet?
+ toggle_award_emoji_snippet_note_path(awardable.noteable, awardable)
+ else
+ toggle_award_emoji_namespace_project_note_path(@project.namespace, @project, awardable.id)
+ end
else
url_for([:toggle_award_emoji, @project.namespace.becomes(Namespace), @project, awardable])
end
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 0b80747f001..377b080b3c6 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -217,11 +217,21 @@ module BlobHelper
end
def copy_blob_source_button(blob)
+ return unless blob.rendered_as_text?(ignore_errors: false)
+
clipboard_button(target: ".blob-content[data-blob-id='#{blob.id}']", class: "btn btn-sm js-copy-blob-source-btn", title: "Copy source to clipboard")
end
- def open_raw_blob_button
- link_to icon('file-code-o'), blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: 'Open raw', data: { container: 'body' }
+ def open_raw_blob_button(blob)
+ if blob.raw_binary?
+ icon = icon('download')
+ title = 'Download'
+ else
+ icon = icon('file-code-o')
+ title = 'Open raw'
+ end
+
+ link_to icon, blob_raw_url, class: 'btn btn-sm has-tooltip', target: '_blank', rel: 'noopener noreferrer', title: title, data: { container: 'body' }
end
def blob_render_error_reason(viewer)
diff --git a/app/helpers/merge_requests_helper.rb b/app/helpers/merge_requests_helper.rb
index e347f61fb8d..2614cdfe90e 100644
--- a/app/helpers/merge_requests_helper.rb
+++ b/app/helpers/merge_requests_helper.rb
@@ -1,6 +1,6 @@
module MergeRequestsHelper
def new_mr_path_from_push_event(event)
- target_project = event.project.forked_from_project || event.project
+ target_project = event.project.default_merge_request_target
new_namespace_project_merge_request_path(
event.project.namespace,
event.project,
@@ -127,6 +127,10 @@ module MergeRequestsHelper
end
end
+ def target_projects(project)
+ [project, project.default_merge_request_target].uniq
+ end
+
def merge_request_button_visibility(merge_request, closed)
return 'hidden' if merge_request.closed? == closed || (merge_request.merged? == closed && !merge_request.closed?) || merge_request.closed_without_fork?
end
diff --git a/app/helpers/snippets_helper.rb b/app/helpers/snippets_helper.rb
index 979264c9421..2fd64b3441e 100644
--- a/app/helpers/snippets_helper.rb
+++ b/app/helpers/snippets_helper.rb
@@ -8,6 +8,14 @@ module SnippetsHelper
end
end
+ def download_snippet_path(snippet)
+ if snippet.project_id
+ raw_namespace_project_snippet_path(@project.namespace, @project, snippet, inline: false)
+ else
+ raw_snippet_path(snippet, inline: false)
+ end
+ end
+
# Return the path of a snippets index for a user or for a project
#
# @returns String, path to snippet index
diff --git a/app/models/concerns/cache_markdown_field.rb b/app/models/concerns/cache_markdown_field.rb
index f033028c4e5..eb32bf3d32a 100644
--- a/app/models/concerns/cache_markdown_field.rb
+++ b/app/models/concerns/cache_markdown_field.rb
@@ -78,6 +78,9 @@ module CacheMarkdownField
def cached_html_up_to_date?(markdown_field)
html_field = cached_markdown_fields.html_field(markdown_field)
+ cached = !cached_html_for(markdown_field).nil? && !__send__(markdown_field).nil?
+ return false unless cached
+
markdown_changed = attribute_changed?(markdown_field) || false
html_changed = attribute_changed?(html_field) || false
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 9d2288c311e..365fa4f1e70 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -100,6 +100,7 @@ class MergeRequest < ActiveRecord::Base
validates :merge_user, presence: true, if: :merge_when_pipeline_succeeds?, unless: :importing?
validate :validate_branches, unless: [:allow_broken, :importing?, :closed_without_fork?]
validate :validate_fork, unless: :closed_without_fork?
+ validate :validate_target_project, on: :create
scope :by_source_or_target_branch, ->(branch_name) do
where("source_branch = :branch OR target_branch = :branch", branch: branch_name)
@@ -330,6 +331,12 @@ class MergeRequest < ActiveRecord::Base
end
end
+ def validate_target_project
+ return true if target_project.merge_requests_enabled?
+
+ errors.add :base, 'Target project has disabled merge requests'
+ end
+
def validate_fork
return true unless target_project && source_project
return true if target_project == source_project
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index 9bfa731785f..397dc7a25ab 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -33,7 +33,7 @@ class Namespace < ActiveRecord::Base
validates :path,
presence: true,
length: { maximum: 255 },
- namespace: true
+ dynamic_path: true
validate :nesting_level_allowed
@@ -220,6 +220,10 @@ class Namespace < ActiveRecord::Base
Project.inside_path(full_path)
end
+ def has_parent?
+ parent.present?
+ end
+
private
def repository_storage_paths
diff --git a/app/models/project.rb b/app/models/project.rb
index c7dc562c238..6a8f8c3500f 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -196,13 +196,14 @@ class Project < ActiveRecord::Base
message: Gitlab::Regex.project_name_regex_message }
validates :path,
presence: true,
- project_path: true,
+ dynamic_path: true,
length: { maximum: 255 },
format: { with: Gitlab::Regex.project_path_regex,
- message: Gitlab::Regex.project_path_regex_message }
+ message: Gitlab::Regex.project_path_regex_message },
+ uniqueness: { scope: :namespace_id }
+
validates :namespace, presence: true
validates :name, uniqueness: { scope: :namespace_id }
- validates :path, uniqueness: { scope: :namespace_id }
validates :import_url, addressable_url: true, if: :external_import?
validates :import_url, importable_url: true, if: [:external_import?, :import_url_changed?]
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
@@ -1314,6 +1315,14 @@ class Project < ActiveRecord::Base
namespace_id_changed?
end
+ def default_merge_request_target
+ if forked_from_project&.merge_requests_enabled?
+ forked_from_project
+ else
+ self
+ end
+ end
+
alias_method :name_with_namespace, :full_name
alias_method :human_name, :full_name
alias_method :path_with_namespace, :full_path
diff --git a/app/models/repository.rb b/app/models/repository.rb
index feabfa111fb..ba34d570dbd 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -505,14 +505,8 @@ class Repository
delegate :tag_names, to: :raw_repository
cache_method :tag_names, fallback: []
- def branch_count
- branches.size
- end
+ delegate :branch_count, :tag_count, to: :raw_repository
cache_method :branch_count, fallback: 0
-
- def tag_count
- raw_repository.rugged.tags.count
- end
cache_method :tag_count, fallback: 0
def avatar
diff --git a/app/models/user.rb b/app/models/user.rb
index bd9c9f99663..2b7ebe6c1a7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -118,7 +118,7 @@ class User < ActiveRecord::Base
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
validates :username,
- namespace: true,
+ dynamic_path: true,
presence: true,
uniqueness: { case_sensitive: false }
diff --git a/app/serializers/deployment_entity.rb b/app/serializers/deployment_entity.rb
index d610fbe0c8a..8b3de1bed0f 100644
--- a/app/serializers/deployment_entity.rb
+++ b/app/serializers/deployment_entity.rb
@@ -18,8 +18,10 @@ class DeploymentEntity < Grape::Entity
end
end
+ expose :created_at
expose :tag
expose :last?
+
expose :user, using: UserEntity
expose :commit, using: CommitEntity
expose :deployable, using: BuildEntity
diff --git a/app/serializers/deployment_serializer.rb b/app/serializers/deployment_serializer.rb
new file mode 100644
index 00000000000..cba5c3f311f
--- /dev/null
+++ b/app/serializers/deployment_serializer.rb
@@ -0,0 +1,8 @@
+class DeploymentSerializer < BaseSerializer
+ entity DeploymentEntity
+
+ def represent_concise(resource, opts = {})
+ opts[:only] = [:iid, :id, :sha, :created_at, :tag, :last?, :id, ref: [:name]]
+ represent(resource, opts)
+ end
+end
diff --git a/app/serializers/status_entity.rb b/app/serializers/status_entity.rb
index 944472f3e51..188c3747f18 100644
--- a/app/serializers/status_entity.rb
+++ b/app/serializers/status_entity.rb
@@ -7,6 +7,9 @@ class StatusEntity < Grape::Entity
expose :details_path
expose :favicon do |status|
- ActionController::Base.helpers.image_path(File.join('ci_favicons', "#{status.favicon}.ico"))
+ dir = 'ci_favicons'
+ dir = File.join(dir, 'dev') if Rails.env.development?
+
+ ActionController::Base.helpers.image_path(File.join(dir, "#{status.favicon}.ico"))
end
end
diff --git a/app/services/merge_requests/build_service.rb b/app/services/merge_requests/build_service.rb
index d45da5180e1..bc0e7ad4e39 100644
--- a/app/services/merge_requests/build_service.rb
+++ b/app/services/merge_requests/build_service.rb
@@ -28,7 +28,7 @@ module MergeRequests
def find_target_project
return target_project if target_project.present? && can?(current_user, :read_project, target_project)
- project.forked_from_project || project
+ project.default_merge_request_target
end
def find_target_branch
diff --git a/app/services/todo_service.rb b/app/services/todo_service.rb
index b6e88b0280f..8ae61694b50 100644
--- a/app/services/todo_service.rb
+++ b/app/services/todo_service.rb
@@ -281,7 +281,7 @@ class TodoService
def attributes_for_target(target)
attributes = {
- project_id: target.project.id,
+ project_id: target&.project&.id,
target_id: target.id,
target_type: target.class.name,
commit_id: nil
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
new file mode 100644
index 00000000000..226eb6b313c
--- /dev/null
+++ b/app/validators/dynamic_path_validator.rb
@@ -0,0 +1,208 @@
+# DynamicPathValidator
+#
+# Custom validator for GitLab path values.
+# These paths are assigned to `Namespace` (& `Group` as a subclass) & `Project`
+#
+# Values are checked for formatting and exclusion from a list of reserved path
+# names.
+class DynamicPathValidator < ActiveModel::EachValidator
+ # All routes that appear on the top level must be listed here.
+ # This will make sure that groups cannot be created with these names
+ # as these routes would be masked by the paths already in place.
+ #
+ # Example:
+ # /api/api-project
+ #
+ # the path `api` shouldn't be allowed because it would be masked by `api/*`
+ #
+ TOP_LEVEL_ROUTES = %w[
+ -
+ .well-known
+ abuse_reports
+ admin
+ all
+ api
+ assets
+ autocomplete
+ ci
+ dashboard
+ explore
+ files
+ groups
+ health_check
+ help
+ hooks
+ import
+ invites
+ issues
+ jwt
+ koding
+ member
+ merge_requests
+ new
+ notes
+ notification_settings
+ oauth
+ profile
+ projects
+ public
+ repository
+ robots.txt
+ s
+ search
+ sent_notifications
+ services
+ snippets
+ teams
+ u
+ unicorn_test
+ unsubscribes
+ uploads
+ users
+ ].freeze
+
+ # This list should contain all words following `/*namespace_id/:project_id` in
+ # routes that contain a second wildcard.
+ #
+ # Example:
+ # /*namespace_id/:project_id/badges/*ref/build
+ #
+ # If `badges` was allowed as a project/group name, we would not be able to access the
+ # `badges` route for those projects:
+ #
+ # Consider a namespace with path `foo/bar` and a project called `badges`.
+ # The route to the build badge would then be `/foo/bar/badges/badges/master/build.svg`
+ #
+ # When accessing this path the route would be matched to the `badges` path
+ # with the following params:
+ # - namespace_id: `foo`
+ # - project_id: `bar`
+ # - ref: `badges/master`
+ #
+ # Failing to find the project, this would result in a 404.
+ #
+ # By rejecting `badges` the router can _count_ on the fact that `badges` will
+ # be preceded by the `namespace/project`.
+ WILDCARD_ROUTES = %w[
+ badges
+ blame
+ blob
+ builds
+ commits
+ create
+ create_dir
+ edit
+ environments/folders
+ files
+ find_file
+ gitlab-lfs/objects
+ info/lfs/objects
+ new
+ preview
+ raw
+ refs
+ tree
+ update
+ wikis
+ ].freeze
+
+ # These are all the paths that follow `/groups/*id/ or `/groups/*group_id`
+ # We need to reject these because we have a `/groups/*id` page that is the same
+ # as the `/*id`.
+ #
+ # If we would allow a subgroup to be created with the name `activity` then
+ # this group would not be accessible through `/groups/parent/activity` since
+ # this would map to the activity-page of it's parent.
+ GROUP_ROUTES = %w[
+ activity
+ avatar
+ edit
+ group_members
+ issues
+ labels
+ merge_requests
+ milestones
+ projects
+ subgroups
+ ].freeze
+
+ CHILD_ROUTES = (WILDCARD_ROUTES | GROUP_ROUTES).freeze
+
+ def self.without_reserved_wildcard_paths_regex
+ @without_reserved_wildcard_paths_regex ||= regex_excluding_child_paths(WILDCARD_ROUTES)
+ end
+
+ def self.without_reserved_child_paths_regex
+ @without_reserved_child_paths_regex ||= regex_excluding_child_paths(CHILD_ROUTES)
+ end
+
+ # This is used to validate a full path.
+ # It doesn't match paths
+ # - Starting with one of the top level words
+ # - Containing one of the child level words in the middle of a path
+ def self.regex_excluding_child_paths(child_routes)
+ reserved_top_level_words = Regexp.union(TOP_LEVEL_ROUTES)
+ not_starting_in_reserved_word = %r{\A/?(?!(#{reserved_top_level_words})(/|\z))}
+
+ reserved_child_level_words = Regexp.union(child_routes)
+ not_containing_reserved_child = %r{(?!\S+/(#{reserved_child_level_words})(/|\z))}
+
+ %r{#{not_starting_in_reserved_word}
+ #{not_containing_reserved_child}
+ #{Gitlab::Regex.full_namespace_regex}}x
+ end
+
+ def self.valid?(path)
+ path =~ Gitlab::Regex.full_namespace_regex && !full_path_reserved?(path)
+ end
+
+ def self.full_path_reserved?(path)
+ path = path.to_s.downcase
+ _project_part, namespace_parts = path.reverse.split('/', 2).map(&:reverse)
+
+ wildcard_reserved?(path) || child_reserved?(namespace_parts)
+ end
+
+ def self.child_reserved?(path)
+ return false unless path
+
+ path !~ without_reserved_child_paths_regex
+ end
+
+ def self.wildcard_reserved?(path)
+ return false unless path
+
+ path !~ without_reserved_wildcard_paths_regex
+ end
+
+ delegate :full_path_reserved?,
+ :child_reserved?,
+ to: :class
+
+ def path_reserved_for_record?(record, value)
+ full_path = record.respond_to?(:full_path) ? record.full_path : value
+
+ # For group paths the entire path cannot contain a reserved child word
+ # The path doesn't contain the last `_project_part` so we need to validate
+ # if the entire path.
+ # Example:
+ # A *group* with full path `parent/activity` is reserved.
+ # A *project* with full path `parent/activity` is allowed.
+ if record.is_a? Group
+ child_reserved?(full_path)
+ else
+ full_path_reserved?(full_path)
+ end
+ end
+
+ def validate_each(record, attribute, value)
+ unless value =~ Gitlab::Regex.namespace_regex
+ record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
+ return
+ end
+
+ if path_reserved_for_record?(record, value)
+ record.errors.add(attribute, "#{value} is a reserved name")
+ end
+ end
+end
diff --git a/app/validators/namespace_validator.rb b/app/validators/namespace_validator.rb
deleted file mode 100644
index 77ca033e97f..00000000000
--- a/app/validators/namespace_validator.rb
+++ /dev/null
@@ -1,73 +0,0 @@
-# NamespaceValidator
-#
-# Custom validator for GitLab namespace values.
-#
-# Values are checked for formatting and exclusion from a list of reserved path
-# names.
-class NamespaceValidator < ActiveModel::EachValidator
- RESERVED = %w[
- .well-known
- admin
- all
- assets
- ci
- dashboard
- files
- groups
- help
- hooks
- issues
- merge_requests
- new
- notes
- profile
- projects
- public
- repository
- robots.txt
- s
- search
- services
- snippets
- teams
- u
- unsubscribes
- users
- ].freeze
-
- WILDCARD_ROUTES = %w[tree commits wikis new edit create update logs_tree
- preview blob blame raw files create_dir find_file
- artifacts graphs refs badges].freeze
-
- STRICT_RESERVED = (RESERVED + WILDCARD_ROUTES).freeze
-
- def self.valid?(value)
- !reserved?(value) && follow_format?(value)
- end
-
- def self.reserved?(value, strict: false)
- if strict
- STRICT_RESERVED.include?(value)
- else
- RESERVED.include?(value)
- end
- end
-
- def self.follow_format?(value)
- value =~ Gitlab::Regex.namespace_regex
- end
-
- delegate :reserved?, :follow_format?, to: :class
-
- def validate_each(record, attribute, value)
- unless follow_format?(value)
- record.errors.add(attribute, Gitlab::Regex.namespace_regex_message)
- end
-
- strict = record.is_a?(Group) && record.parent_id
-
- if reserved?(value, strict: strict)
- record.errors.add(attribute, "#{value} is a reserved name")
- end
- end
-end
diff --git a/app/validators/project_path_validator.rb b/app/validators/project_path_validator.rb
deleted file mode 100644
index ee2ae65be7b..00000000000
--- a/app/validators/project_path_validator.rb
+++ /dev/null
@@ -1,35 +0,0 @@
-# ProjectPathValidator
-#
-# Custom validator for GitLab project path values.
-#
-# Values are checked for formatting and exclusion from a list of reserved path
-# names.
-class ProjectPathValidator < ActiveModel::EachValidator
- # All project routes with wildcard argument must be listed here.
- # Otherwise it can lead to routing issues when route considered as project name.
- #
- # Example:
- # /group/project/tree/deploy_keys
- #
- # without tree as reserved name routing can match 'group/project' as group name,
- # 'tree' as project name and 'deploy_keys' as route.
- #
- RESERVED = (NamespaceValidator::STRICT_RESERVED -
- %w[dashboard help ci admin search notes services assets profile public]).freeze
-
- def self.valid?(value)
- !reserved?(value)
- end
-
- def self.reserved?(value)
- RESERVED.include?(value)
- end
-
- delegate :reserved?, to: :class
-
- def validate_each(record, attribute, value)
- if reserved?(value)
- record.errors.add(attribute, "#{value} is a reserved name")
- end
- end
-end
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 8c9fdc9ae42..53f0a1e7fde 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -73,6 +73,12 @@
= container_reg
%span.light.pull-right
= boolean_to_icon Gitlab.config.registry.enabled
+ - gitlab_pages = 'GitLab Pages'
+ - gitlab_pages_enabled = Gitlab.config.pages.enabled
+ %p{ "aria-label" => "#{gitlab_pages}: status " + (gitlab_pages_enabled ? "on" : "off") }
+ = gitlab_pages
+ %span.light.pull-right
+ = boolean_to_icon gitlab_pages_enabled
.col-md-4
%h4
diff --git a/app/views/admin/hooks/_form.html.haml b/app/views/admin/hooks/_form.html.haml
new file mode 100644
index 00000000000..6217d5fb135
--- /dev/null
+++ b/app/views/admin/hooks/_form.html.haml
@@ -0,0 +1,40 @@
+= form_errors(hook)
+
+.form-group
+ = form.label :url, 'URL', class: 'control-label'
+ .col-sm-10
+ = form.text_field :url, class: 'form-control'
+.form-group
+ = form.label :token, 'Secret Token', class: 'control-label'
+ .col-sm-10
+ = form.text_field :token, class: 'form-control'
+ %p.help-block
+ Use this token to validate received payloads
+.form-group
+ = form.label :url, 'Trigger', class: 'control-label'
+ .col-sm-10.prepend-top-10
+ %div
+ System hook will be triggered on set of events like creating project
+ or adding ssh key. But you can also enable extra triggers like Push events.
+
+ .prepend-top-default
+ = form.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This url will be triggered by a push to the repository
+ %div
+ = form.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This url will be triggered when a new tag is pushed to the repository
+.form-group
+ = form.label :enable_ssl_verification, 'SSL verification', class: 'control-label checkbox'
+ .col-sm-10
+ .checkbox
+ = form.label :enable_ssl_verification do
+ = form.check_box :enable_ssl_verification
+ %strong Enable SSL verification
diff --git a/app/views/admin/hooks/edit.html.haml b/app/views/admin/hooks/edit.html.haml
new file mode 100644
index 00000000000..0777f5e2629
--- /dev/null
+++ b/app/views/admin/hooks/edit.html.haml
@@ -0,0 +1,14 @@
+- page_title 'Edit System Hook'
+%h3.page-title
+ Edit System Hook
+
+%p.light
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
+ used for binding events when GitLab creates a User or Project.
+
+%hr
+
+= form_for @hook, as: :hook, url: admin_hook_path, html: { class: 'form-horizontal' } do |f|
+ = render partial: 'form', locals: { form: f, hook: @hook }
+ .form-actions
+ = f.submit 'Save changes', class: 'btn btn-create'
diff --git a/app/views/admin/hooks/index.html.haml b/app/views/admin/hooks/index.html.haml
index d9c7948763a..71117758921 100644
--- a/app/views/admin/hooks/index.html.haml
+++ b/app/views/admin/hooks/index.html.haml
@@ -1,57 +1,17 @@
-- page_title "System Hooks"
+- page_title 'System Hooks'
%h3.page-title
System hooks
%p.light
- #{link_to "System hooks ", help_page_path("system_hooks/system_hooks"), class: "vlink"} can be
+ #{link_to 'System hooks ', help_page_path('system_hooks/system_hooks'), class: 'vlink'} can be
used for binding events when GitLab creates a User or Project.
%hr
-
= form_for @hook, as: :hook, url: admin_hooks_path, html: { class: 'form-horizontal' } do |f|
- = form_errors(@hook)
-
- .form-group
- = f.label :url, 'URL', class: 'control-label'
- .col-sm-10
- = f.text_field :url, class: 'form-control'
- .form-group
- = f.label :token, 'Secret Token', class: 'control-label'
- .col-sm-10
- = f.text_field :token, class: 'form-control'
- %p.help-block
- Use this token to validate received payloads
- .form-group
- = f.label :url, "Trigger", class: 'control-label'
- .col-sm-10.prepend-top-10
- %div
- System hook will be triggered on set of events like creating project
- or adding ssh key. But you can also enable extra triggers like Push events.
-
- .prepend-top-default
- = f.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This url will be triggered by a push to the repository
- %div
- = f.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This url will be triggered when a new tag is pushed to the repository
- .form-group
- = f.label :enable_ssl_verification, "SSL verification", class: 'control-label checkbox'
- .col-sm-10
- .checkbox
- = f.label :enable_ssl_verification do
- = f.check_box :enable_ssl_verification
- %strong Enable SSL verification
+ = render partial: 'form', locals: { form: f, hook: @hook }
.form-actions
- = f.submit "Add system hook", class: "btn btn-create"
+ = f.submit 'Add system hook', class: 'btn btn-create'
%hr
- if @hooks.any?
@@ -62,11 +22,12 @@
- @hooks.each do |hook|
%li
.controls
- = link_to 'Test hook', admin_hook_test_path(hook), class: "btn btn-sm"
- = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: "btn btn-remove btn-sm"
+ = link_to 'Test hook', test_admin_hook_path(hook), class: 'btn btn-sm'
+ = link_to 'Edit', edit_admin_hook_path(hook), class: 'btn btn-sm'
+ = link_to 'Remove', admin_hook_path(hook), data: { confirm: 'Are you sure?' }, method: :delete, class: 'btn btn-remove btn-sm'
.monospace= hook.url
%div
- %w(push_events tag_push_events issues_events note_events merge_requests_events build_events).each do |trigger|
- if hook.send(trigger)
%span.label.label-gray= trigger.titleize
- %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+ %span.label.label-gray SSL Verification: #{hook.enable_ssl_verification ? 'enabled' : 'disabled'}
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index 0e848386ebb..4594c52b34b 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -2,10 +2,10 @@
%ul.nav-links
= nav_link(page: dashboard_groups_path) do
= link_to dashboard_groups_path, title: 'Your groups' do
- Your Groups
+ Your groups
= nav_link(page: explore_groups_path) do
- = link_to explore_groups_path, title: 'Explore groups' do
- Explore Groups
+ = link_to explore_groups_path, title: 'Explore public groups' do
+ Explore public groups
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
diff --git a/app/views/discussions/_notes.html.haml b/app/views/discussions/_notes.html.haml
index 34789808f10..964473ee3e0 100644
--- a/app/views/discussions/_notes.html.haml
+++ b/app/views/discussions/_notes.html.haml
@@ -1,6 +1,6 @@
.discussion-notes
%ul.notes{ data: { discussion_id: discussion.id } }
- = render partial: "projects/notes/note", collection: discussion.notes, as: :note
+ = render partial: "shared/notes/note", collection: discussion.notes, as: :note
- if current_user
.discussion-reply-holder
diff --git a/app/views/explore/groups/index.html.haml b/app/views/explore/groups/index.html.haml
index bb2cd0d44c8..ffe07b217a7 100644
--- a/app/views/explore/groups/index.html.haml
+++ b/app/views/explore/groups/index.html.haml
@@ -7,6 +7,15 @@
= render 'explore/head'
= render 'nav'
+- if cookies[:explore_groups_landing_dismissed] != 'true'
+ .explore-groups.landing.content-block.js-explore-groups-landing.hidden
+ %button.dismiss-button{ type: 'button', 'aria-label' => 'Dismiss' }= icon('times')
+ .svg-container
+ = custom_icon('icon_explore_groups_splash')
+ .inner-content
+ %p Below you will find all the groups that are public.
+ %p You can easily contribute to them by requesting to join these groups.
+
- if @groups.present?
= render 'groups'
- else
diff --git a/app/views/projects/blob/_header.html.haml b/app/views/projects/blob/_header.html.haml
index 638f8cef3bd..219dc14645b 100644
--- a/app/views/projects/blob/_header.html.haml
+++ b/app/views/projects/blob/_header.html.haml
@@ -15,8 +15,8 @@
= render 'projects/blob/viewer_switcher', blob: blob unless blame
.btn-group{ role: "group" }<
- = copy_blob_source_button(blob) if !blame && blob.rendered_as_text?(ignore_errors: false)
- = open_raw_blob_button
+ = copy_blob_source_button(blob) unless blame
+ = open_raw_blob_button(blob)
= view_on_environment_button(@commit.sha, @path, @environment) if @environment
.btn-group{ role: "group" }<
diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml
index 85e442e115c..50e0bad3ccf 100644
--- a/app/views/projects/empty.html.haml
+++ b/app/views/projects/empty.html.haml
@@ -60,7 +60,7 @@
git init
git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')}
git add .
- git commit
+ git commit -m "Initial commit"
git push -u origin master
%fieldset
diff --git a/app/views/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index 766f119116f..e8f8fbbcf09 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -5,7 +5,7 @@
= page_specific_javascript_bundle_tag('monitoring')
= render "projects/pipelines/head"
-.prometheus-container{ class: container_class, 'data-has-metrics': "#{@environment.has_metrics?}" }
+#js-metrics.prometheus-container{ class: container_class, data: { has_metrics: "#{@environment.has_metrics?}", deployment_endpoint: namespace_project_environment_deployments_path(@project.namespace, @project, @environment, format: :json) } }
.top-area
.row
.col-sm-6
diff --git a/app/views/projects/hooks/_index.html.haml b/app/views/projects/hooks/_index.html.haml
index 8faad351463..676b7c345bc 100644
--- a/app/views/projects/hooks/_index.html.haml
+++ b/app/views/projects/hooks/_index.html.haml
@@ -1 +1,23 @@
-= render 'shared/web_hooks/form', hook: @hook, hooks: @hooks, url_components: [@project.namespace.becomes(Namespace), @project]
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
+ %p
+ #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
+ used for binding events when something is happening within the project.
+
+ .col-lg-9.append-bottom-default
+ = form_for @hook, as: :hook, url: polymorphic_path([@project.namespace.becomes(Namespace), @project, :hooks]) do |f|
+ = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
+ = f.submit 'Add webhook', class: 'btn btn-create'
+
+ %hr
+ %h5.prepend-top-default
+ Webhooks (#{@hooks.count})
+ - if @hooks.any?
+ %ul.well-list
+ - @hooks.each do |hook|
+ = render 'project_hook', hook: hook
+ - else
+ %p.settings-message.text-center.append-bottom-0
+ No webhooks found, add one in the form above.
diff --git a/app/views/projects/hooks/edit.html.haml b/app/views/projects/hooks/edit.html.haml
new file mode 100644
index 00000000000..7998713be1f
--- /dev/null
+++ b/app/views/projects/hooks/edit.html.haml
@@ -0,0 +1,14 @@
+= render 'projects/settings/head'
+
+.row.prepend-top-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
+ %p
+ #{link_to 'Webhooks', help_page_path('user/project/integrations/webhooks')} can be
+ used for binding events when something is happening within the project.
+ .col-lg-9.append-bottom-default
+ = form_for [@project.namespace.becomes(Namespace), @project, @hook], as: :hook, url: namespace_project_hook_path do |f|
+ = render partial: 'shared/web_hooks/form', locals: { form: f, hook: @hook }
+ = f.submit 'Save changes', class: 'btn btn-create'
+
diff --git a/app/views/projects/merge_requests/_new_compare.html.haml b/app/views/projects/merge_requests/_new_compare.html.haml
index 8d134aaac67..9cf24e10842 100644
--- a/app/views/projects/merge_requests/_new_compare.html.haml
+++ b/app/views/projects/merge_requests/_new_compare.html.haml
@@ -38,7 +38,7 @@
.panel-heading
Target branch
.panel-body.clearfix
- - projects = @project.forked_from_project.nil? ? [@project] : [@project, @project.forked_from_project]
+ - projects = target_projects(@project)
.merge-request-select.dropdown
= f.hidden_field :target_project_id
= dropdown_toggle f.object.target_project.path_with_namespace, { toggle: "dropdown", field_name: "#{f.object_name}[target_project_id]", disabled: @merge_request.persisted? }, { toggle_class: "js-compare-dropdown js-target-project" }
diff --git a/app/views/projects/notes/_actions.html.haml b/app/views/projects/notes/_actions.html.haml
new file mode 100644
index 00000000000..718b52dd82e
--- /dev/null
+++ b/app/views/projects/notes/_actions.html.haml
@@ -0,0 +1,44 @@
+- access = note_max_access_for_user(note)
+- if access
+ %span.note-role= access
+
+- if note.resolvable?
+ - can_resolve = can?(current_user, :resolve_note, note)
+ %resolve-btn{ "project-path" => project_path(note.project),
+ "discussion-id" => note.discussion_id(@noteable),
+ ":note-id" => note.id,
+ ":resolved" => note.resolved?,
+ ":can-resolve" => can_resolve,
+ ":author-name" => "'#{j(note.author.name)}'",
+ "author-avatar" => note.author.avatar_url,
+ ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
+ ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
+ "v-show" => "#{can_resolve || note.resolved?}",
+ "inline-template" => true,
+ "ref" => "note_#{note.id}" }
+
+ %button.note-action-button.line-resolve-btn{ type: "button",
+ class: ("is-disabled" unless can_resolve),
+ ":class" => "{ 'is-active': isResolved }",
+ ":aria-label" => "buttonText",
+ "@click" => "resolve",
+ ":title" => "buttonText",
+ ":ref" => "'button'" }
+
+ = icon('spin spinner', 'v-show' => 'loading', class: 'loading', 'aria-hidden' => 'true', 'aria-label' => 'Loading')
+ %div{ 'v-show' => '!loading' }= render 'shared/icons/icon_status_success.svg'
+
+- if current_user
+ - if note.emoji_awardable?
+ - user_authored = note.user_authored?(current_user)
+ = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
+ = icon('spinner spin')
+ %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
+ %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
+ %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
+
+ - if note_editable
+ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
+ = icon('pencil', class: 'link-highlight')
+ = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
+ = icon('trash-o', class: 'danger-highlight')
diff --git a/app/views/projects/notes/_edit.html.haml b/app/views/projects/notes/_edit.html.haml
new file mode 100644
index 00000000000..f1e251d65b7
--- /dev/null
+++ b/app/views/projects/notes/_edit.html.haml
@@ -0,0 +1,3 @@
+.original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
+ #{note.note}
+%textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
diff --git a/app/views/projects/notes/_note.html.haml b/app/views/projects/notes/_note.html.haml
deleted file mode 100644
index 7afccb3900a..00000000000
--- a/app/views/projects/notes/_note.html.haml
+++ /dev/null
@@ -1,101 +0,0 @@
-- return unless note.author
-- return if note.cross_reference_not_visible_for?(current_user)
-
-- note_editable = note_editable?(note)
-%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
- .timeline-entry-inner
- .timeline-icon
- - if note.system
- = icon_for_system_note(note)
- - else
- %a{ href: user_path(note.author) }
- = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
- .timeline-content
- .note-header
- .note-header-info
- %a{ href: user_path(note.author) }
- %span.hidden-xs
- = sanitize(note.author.name)
- %span.note-headline-light
- = note.author.to_reference
- %span.note-headline-light
- %span.note-headline-meta
- - unless note.system
- commented
- - if note.system
- %span.system-note-message
- = note.redacted_note_html
- %a{ href: "##{dom_id(note)}" }
- = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
- - unless note.system?
- .note-actions
- - access = note_max_access_for_user(note)
- - if access
- %span.note-role= access
-
- - if note.resolvable?
- - can_resolve = can?(current_user, :resolve_note, note)
- %resolve-btn{ "project-path" => project_path(note.project),
- "discussion-id" => note.discussion_id(@noteable),
- ":note-id" => note.id,
- ":resolved" => note.resolved?,
- ":can-resolve" => can_resolve,
- ":author-name" => "'#{j(note.author.name)}'",
- "author-avatar" => note.author.avatar_url,
- ":note-truncated" => "'#{j(truncate(note.note, length: 17))}'",
- ":resolved-by" => "'#{j(note.resolved_by.try(:name))}'",
- "v-show" => "#{can_resolve || note.resolved?}",
- "inline-template" => true,
- "ref" => "note_#{note.id}" }
-
- %button.note-action-button.line-resolve-btn{ type: "button",
- class: ("is-disabled" unless can_resolve),
- ":class" => "{ 'is-active': isResolved }",
- ":aria-label" => "buttonText",
- "@click" => "resolve",
- ":title" => "buttonText",
- ":ref" => "'button'" }
-
- = icon("spin spinner", "v-show" => "loading", class: 'loading')
- %div{ 'v-show' => '!loading' }= render "shared/icons/icon_status_success.svg"
-
- - if current_user
- - if note.emoji_awardable?
- - user_authored = note.user_authored?(current_user)
- = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
- = icon('spinner spin')
- %span{ class: "link-highlight award-control-icon-neutral" }= custom_icon('emoji_slightly_smiling_face')
- %span{ class: "link-highlight award-control-icon-positive" }= custom_icon('emoji_smiley')
- %span{ class: "link-highlight award-control-icon-super-positive" }= custom_icon('emoji_smile')
-
- - if note_editable
- = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
- = icon('pencil', class: 'link-highlight')
- = link_to namespace_project_note_path(note.project.namespace, note.project, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
- = icon('trash-o', class: 'danger-highlight')
- .note-body{ class: note_editable ? 'js-task-list-container' : '' }
- .note-text.md
- = note.redacted_note_html
- = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
- - if note_editable
- .original-note-content.hidden{ data: { post_url: namespace_project_note_path(@project.namespace, @project, note), target_id: note.noteable.id, target_type: note.noteable.class.name.underscore } }
- #{note.note}
- %textarea.hidden.js-task-list-field.original-task-list{ data: {update_url: namespace_project_note_path(@project.namespace, @project, note) } }= note.note
- .note-awards
- = render 'award_emoji/awards_block', awardable: note, inline: false
- - if note.system
- .system-note-commit-list-toggler
- Toggle commit list
- %i.fa.fa-angle-down
- - if note.attachment.url
- .note-attachment
- - if note.attachment.image?
- = link_to note.attachment.url, target: '_blank' do
- = image_tag note.attachment.url, class: 'note-image-attach'
- .attachment
- = link_to note.attachment.url, target: '_blank' do
- = icon('paperclip')
- = note.attachment_identifier
- = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
- title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
- = icon('trash-o', class: 'cred')
diff --git a/app/views/projects/notes/_notes_with_form.html.haml b/app/views/projects/notes/_notes_with_form.html.haml
index 90a150aa74c..555228623cc 100644
--- a/app/views/projects/notes/_notes_with_form.html.haml
+++ b/app/views/projects/notes/_notes_with_form.html.haml
@@ -1,5 +1,5 @@
%ul#notes-list.notes.main-notes-list.timeline
- = render "projects/notes/notes"
+ = render "shared/notes/notes"
= render 'projects/notes/edit_form'
diff --git a/app/views/projects/settings/_head.html.haml b/app/views/projects/settings/_head.html.haml
index e50a543ffa8..5a5ade03624 100644
--- a/app/views/projects/settings/_head.html.haml
+++ b/app/views/projects/settings/_head.html.haml
@@ -14,7 +14,7 @@
%span
Members
- if can_edit
- = nav_link(controller: [:integrations, :services]) do
+ = nav_link(controller: [:integrations, :services, :hooks]) do
= link_to project_settings_integrations_path(@project), title: 'Integrations' do
%span
Integrations
diff --git a/app/views/projects/settings/integrations/_project_hook.html.haml b/app/views/projects/settings/integrations/_project_hook.html.haml
index ceabe2eab3d..8dc276a3bec 100644
--- a/app/views/projects/settings/integrations/_project_hook.html.haml
+++ b/app/views/projects/settings/integrations/_project_hook.html.haml
@@ -9,6 +9,7 @@
.col-md-4.col-lg-5.text-right-lg.prepend-top-5
%span.append-right-10.inline
SSL Verification: #{hook.enable_ssl_verification ? "enabled" : "disabled"}
+ = link_to "Edit", edit_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to "Test", test_namespace_project_hook_path(@project.namespace, @project, hook), class: "btn btn-sm"
= link_to namespace_project_hook_path(@project.namespace, @project, hook), data: { confirm: 'Are you sure?'}, method: :delete, class: "btn btn-transparent" do
%span.sr-only Remove
diff --git a/app/views/shared/icons/_icon_explore_groups_splash.svg b/app/views/shared/icons/_icon_explore_groups_splash.svg
new file mode 100644
index 00000000000..79f17872739
--- /dev/null
+++ b/app/views/shared/icons/_icon_explore_groups_splash.svg
@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" width="62" height="50" viewBox="260 141 62 50" xmlns:xlink="http://www.w3.org/1999/xlink"><defs><path id="a" d="M24.6 7.7H56c3.3 0 6 2.7 6 6V44c0 3.3-2.7 6-6 6H6c-3.3 0-6-2.7-6-6V4.8C0 2 2.2 0 4.8 0h12c1.5 0 3 1 4 2l3.8 5.7z"/><mask id="e" width="62" height="50" x="0" y="0" fill="#fff"><use xlink:href="#a"/></mask><path id="b" d="M4.2 13c3.7 0 4-1.7 4-4.5S7 4.8 4.2 4.8 0 5.8 0 8.5C0 11.3.5 13 4.2 13z"/><mask id="f" width="10.7" height="10.7" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 3.6H9.5v10.7H-1.2z"/><use xlink:href="#b"/></mask><path id="c" d="M4.2 13c3.7 0 4-1.7 4-4.5S7 4.8 4.2 4.8 0 5.8 0 8.5C0 11.3.5 13 4.2 13z"/><mask id="g" width="10.7" height="10.7" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 3.6H9.5v10.7H-1.2z"/><use xlink:href="#c"/></mask><path id="d" d="M5.4 16c4.7 0 5.3-2.3 5.3-6 0-3.5-1.7-4.6-5.3-4.6C1.7 5.4 0 6.4 0 10s.6 6 5.4 6z"/><mask id="h" width="13.1" height="13.1" x="-1.2" y="-1.2"><path fill="#fff" d="M-1.2 4.2h13v13H-1z"/><use xlink:href="#d"/></mask></defs><g fill="none" fill-rule="evenodd" transform="translate(260 141)"><use fill="#FFF" stroke="#EEE" stroke-width="4.8" mask="url(#e)" xlink:href="#a"/><g transform="translate(33.98 22.62)"><use fill="#B5A7DD" xlink:href="#b"/><use stroke="#FFF" stroke-width="2.4" mask="url(#f)" xlink:href="#b"/><ellipse cx="4.2" cy="3" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3" ry="3"/></g><g transform="translate(19.673 22.62)"><use fill="#B5A7DD" xlink:href="#c"/><use stroke="#FFF" stroke-width="2.4" mask="url(#g)" xlink:href="#c"/><ellipse cx="4.2" cy="3" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3" ry="3"/></g><g transform="translate(25.635 21.43)"><use fill="#B5A7DD" xlink:href="#d"/><use stroke="#FFF" stroke-width="2.4" mask="url(#h)" xlink:href="#d"/><ellipse cx="5.4" cy="3.6" fill="#B5A7DD" stroke="#FFF" stroke-width="1.2" rx="3.6" ry="3.6"/></g></g></svg>
diff --git a/app/views/shared/notes/_note.html.haml b/app/views/shared/notes/_note.html.haml
new file mode 100644
index 00000000000..731270d4127
--- /dev/null
+++ b/app/views/shared/notes/_note.html.haml
@@ -0,0 +1,62 @@
+- return unless note.author
+- return if note.cross_reference_not_visible_for?(current_user)
+
+- note_editable = note_editable?(note)
+%li.timeline-entry{ id: dom_id(note), class: ["note", "note-row-#{note.id}", ('system-note' if note.system)], data: {author_id: note.author.id, editable: note_editable, note_id: note.id} }
+ .timeline-entry-inner
+ .timeline-icon
+ - if note.system
+ = icon_for_system_note(note)
+ - else
+ %a{ href: user_path(note.author) }
+ = image_tag avatar_icon(note.author), alt: '', class: 'avatar s40'
+ .timeline-content
+ .note-header
+ .note-header-info
+ %a{ href: user_path(note.author) }
+ %span.hidden-xs
+ = sanitize(note.author.name)
+ %span.note-headline-light
+ = note.author.to_reference
+ %span.note-headline-light
+ %span.note-headline-meta
+ - unless note.system
+ commented
+ - if note.system
+ %span.system-note-message
+ = note.redacted_note_html
+ %a{ href: "##{dom_id(note)}" }
+ = time_ago_with_tooltip(note.created_at, placement: 'bottom', html_class: 'note-created-ago')
+ - unless note.system?
+ .note-actions
+ - if note.for_personal_snippet?
+ = render 'snippets/notes/actions', note: note, note_editable: note_editable
+ - else
+ = render 'projects/notes/actions', note: note, note_editable: note_editable
+ .note-body{ class: note_editable ? 'js-task-list-container' : '' }
+ .note-text.md
+ = note.redacted_note_html
+ = edited_time_ago_with_tooltip(note, placement: 'bottom', html_class: 'note_edited_ago', include_author: true)
+ - if note_editable
+ - if note.for_personal_snippet?
+ = render 'snippets/notes/edit', note: note
+ - else
+ = render 'projects/notes/edit', note: note
+ .note-awards
+ = render 'award_emoji/awards_block', awardable: note, inline: false
+ - if note.system
+ .system-note-commit-list-toggler
+ Toggle commit list
+ %i.fa.fa-angle-down
+ - if note.attachment.url
+ .note-attachment
+ - if note.attachment.image?
+ = link_to note.attachment.url, target: '_blank' do
+ = image_tag note.attachment.url, class: 'note-image-attach'
+ .attachment
+ = link_to note.attachment.url, target: '_blank' do
+ = icon('paperclip')
+ = note.attachment_identifier
+ = link_to delete_attachment_namespace_project_note_path(note.project.namespace, note.project, note),
+ title: 'Delete this attachment', method: :delete, remote: true, data: { confirm: 'Are you sure you want to remove the attachment?' }, class: 'danger js-note-attachment-delete' do
+ = icon('trash-o', class: 'cred')
diff --git a/app/views/projects/notes/_notes.html.haml b/app/views/shared/notes/_notes.html.haml
index 2b2bab09c74..cfdfeeb9e97 100644
--- a/app/views/projects/notes/_notes.html.haml
+++ b/app/views/shared/notes/_notes.html.haml
@@ -1,8 +1,8 @@
- if defined?(@discussions)
- @discussions.each do |discussion|
- if discussion.individual_note?
- = render partial: "projects/notes/note", collection: discussion.notes, as: :note
+ = render partial: "shared/notes/note", collection: discussion.notes, as: :note
- else
= render 'discussions/discussion', discussion: discussion
- else
- = render partial: "projects/notes/note", collection: @notes, as: :note
+ = render partial: "shared/notes/note", collection: @notes, as: :note
diff --git a/app/views/shared/snippets/_blob.html.haml b/app/views/shared/snippets/_blob.html.haml
index 0c481120969..9bcb4544b97 100644
--- a/app/views/shared/snippets/_blob.html.haml
+++ b/app/views/shared/snippets/_blob.html.haml
@@ -16,9 +16,8 @@
.btn-group{ role: "group" }<
= copy_blob_source_button(blob)
- = open_raw_blob_button
+ = open_raw_blob_button(blob)
- - if defined?(download_path) && download_path
- = link_to icon('download'), download_path, class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
+ = link_to icon('download'), download_snippet_path(@snippet), target: '_blank', class: "btn btn-sm has-tooltip", title: 'Download', data: { container: 'body' }
= render 'projects/blob/content', blob: blob
diff --git a/app/views/shared/web_hooks/_form.html.haml b/app/views/shared/web_hooks/_form.html.haml
index ee3be3c789a..37c3e61912c 100644
--- a/app/views/shared/web_hooks/_form.html.haml
+++ b/app/views/shared/web_hooks/_form.html.haml
@@ -1,102 +1,82 @@
-.row.prepend-top-default
- .col-lg-3
- %h4.prepend-top-0
- = page_title
- %p
- #{link_to "Webhooks", help_page_path("user/project/integrations/webhooks")} can be
- used for binding events when something is happening within the project.
- .col-lg-9.append-bottom-default
- = form_for hook, as: :hook, url: polymorphic_path(url_components + [:hooks]) do |f|
- = form_errors(hook)
+= form_errors(hook)
- .form-group
- = f.label :url, "URL", class: 'label-light'
- = f.text_field :url, class: "form-control", placeholder: 'http://example.com/trigger-ci.json'
- .form-group
- = f.label :token, "Secret Token", class: 'label-light'
- = f.text_field :token, class: "form-control", placeholder: ''
- %p.help-block
- Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
- .form-group
- = f.label :url, "Trigger", class: 'label-light'
- %ul.list-unstyled
- %li
- = f.check_box :push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :push_events, class: 'list-label' do
- %strong Push events
- %p.light
- This URL will be triggered by a push to the repository
- %li
- = f.check_box :tag_push_events, class: 'pull-left'
- .prepend-left-20
- = f.label :tag_push_events, class: 'list-label' do
- %strong Tag push events
- %p.light
- This URL will be triggered when a new tag is pushed to the repository
- %li
- = f.check_box :note_events, class: 'pull-left'
- .prepend-left-20
- = f.label :note_events, class: 'list-label' do
- %strong Comments
- %p.light
- This URL will be triggered when someone adds a comment
- %li
- = f.check_box :issues_events, class: 'pull-left'
- .prepend-left-20
- = f.label :issues_events, class: 'list-label' do
- %strong Issues events
- %p.light
- This URL will be triggered when an issue is created/updated/merged
- %li
- = f.check_box :confidential_issues_events, class: 'pull-left'
- .prepend-left-20
- = f.label :confidential_issues_events, class: 'list-label' do
- %strong Confidential Issues events
- %p.light
- This URL will be triggered when a confidential issue is created/updated/merged
- %li
- = f.check_box :merge_requests_events, class: 'pull-left'
- .prepend-left-20
- = f.label :merge_requests_events, class: 'list-label' do
- %strong Merge Request events
- %p.light
- This URL will be triggered when a merge request is created/updated/merged
- %li
- = f.check_box :build_events, class: 'pull-left'
- .prepend-left-20
- = f.label :build_events, class: 'list-label' do
- %strong Jobs events
- %p.light
- This URL will be triggered when the job status changes
- %li
- = f.check_box :pipeline_events, class: 'pull-left'
- .prepend-left-20
- = f.label :pipeline_events, class: 'list-label' do
- %strong Pipeline events
- %p.light
- This URL will be triggered when the pipeline status changes
- %li
- = f.check_box :wiki_page_events, class: 'pull-left'
- .prepend-left-20
- = f.label :wiki_page_events, class: 'list-label' do
- %strong Wiki Page events
- %p.light
- This URL will be triggered when a wiki page is created/updated
- .form-group
- = f.label :enable_ssl_verification, "SSL verification", class: 'label-light checkbox'
- .checkbox
- = f.label :enable_ssl_verification do
- = f.check_box :enable_ssl_verification
- %strong Enable SSL verification
- = f.submit "Add webhook", class: "btn btn-create"
- %hr
- %h5.prepend-top-default
- Webhooks (#{hooks.count})
- - if hooks.any?
- %ul.well-list
- - hooks.each do |hook|
- = render "project_hook", hook: hook
- - else
- %p.settings-message.text-center.append-bottom-0
- No webhooks found, add one in the form above.
+.form-group
+ = form.label :url, 'URL', class: 'label-light'
+ = form.text_field :url, class: 'form-control', placeholder: 'http://example.com/trigger-ci.json'
+.form-group
+ = form.label :token, 'Secret Token', class: 'label-light'
+ = form.text_field :token, class: 'form-control', placeholder: ''
+ %p.help-block
+ Use this token to validate received payloads. It will be sent with the request in the X-Gitlab-Token HTTP header.
+.form-group
+ = form.label :url, 'Trigger', class: 'label-light'
+ %ul.list-unstyled
+ %li
+ = form.check_box :push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :push_events, class: 'list-label' do
+ %strong Push events
+ %p.light
+ This URL will be triggered by a push to the repository
+ %li
+ = form.check_box :tag_push_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :tag_push_events, class: 'list-label' do
+ %strong Tag push events
+ %p.light
+ This URL will be triggered when a new tag is pushed to the repository
+ %li
+ = form.check_box :note_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :note_events, class: 'list-label' do
+ %strong Comments
+ %p.light
+ This URL will be triggered when someone adds a comment
+ %li
+ = form.check_box :issues_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :issues_events, class: 'list-label' do
+ %strong Issues events
+ %p.light
+ This URL will be triggered when an issue is created/updated/merged
+ %li
+ = form.check_box :confidential_issues_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :confidential_issues_events, class: 'list-label' do
+ %strong Confidential Issues events
+ %p.light
+ This URL will be triggered when a confidential issue is created/updated/merged
+ %li
+ = form.check_box :merge_requests_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :merge_requests_events, class: 'list-label' do
+ %strong Merge Request events
+ %p.light
+ This URL will be triggered when a merge request is created/updated/merged
+ %li
+ = form.check_box :build_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :build_events, class: 'list-label' do
+ %strong Jobs events
+ %p.light
+ This URL will be triggered when the job status changes
+ %li
+ = form.check_box :pipeline_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :pipeline_events, class: 'list-label' do
+ %strong Pipeline events
+ %p.light
+ This URL will be triggered when the pipeline status changes
+ %li
+ = form.check_box :wiki_page_events, class: 'pull-left'
+ .prepend-left-20
+ = form.label :wiki_page_events, class: 'list-label' do
+ %strong Wiki Page events
+ %p.light
+ This URL will be triggered when a wiki page is created/updated
+.form-group
+ = form.label :enable_ssl_verification, 'SSL verification', class: 'label-light checkbox'
+ .checkbox
+ = form.label :enable_ssl_verification do
+ = form.check_box :enable_ssl_verification
+ %strong Enable SSL verification
diff --git a/app/views/snippets/notes/_actions.html.haml b/app/views/snippets/notes/_actions.html.haml
new file mode 100644
index 00000000000..dace11e5474
--- /dev/null
+++ b/app/views/snippets/notes/_actions.html.haml
@@ -0,0 +1,13 @@
+- if current_user
+ - if note.emoji_awardable?
+ - user_authored = note.user_authored?(current_user)
+ = link_to '#', title: 'Award Emoji', class: "note-action-button note-emoji-button js-add-award js-note-emoji #{'js-user-authored' if user_authored}", data: { position: 'right' } do
+ = icon('spinner spin')
+ %span{ class: 'link-highlight award-control-icon-neutral' }= custom_icon('emoji_slightly_smiling_face')
+ %span{ class: 'link-highlight award-control-icon-positive' }= custom_icon('emoji_smiley')
+ %span{ class: 'link-highlight award-control-icon-super-positive' }= custom_icon('emoji_smile')
+ - if note_editable
+ = link_to '#', title: 'Edit comment', class: 'note-action-button js-note-edit' do
+ = icon('pencil', class: 'link-highlight')
+ = link_to snippet_note_path(note.noteable, note), title: 'Remove comment', method: :delete, data: { confirm: 'Are you sure you want to remove this comment?' }, remote: true, class: 'note-action-button js-note-delete danger' do
+ = icon('trash-o', class: 'danger-highlight')
diff --git a/app/views/snippets/notes/_edit.html.haml b/app/views/snippets/notes/_edit.html.haml
new file mode 100644
index 00000000000..e69de29bb2d
--- /dev/null
+++ b/app/views/snippets/notes/_edit.html.haml
diff --git a/app/views/snippets/notes/_notes.html.haml b/app/views/snippets/notes/_notes.html.haml
new file mode 100644
index 00000000000..f07d6b8c126
--- /dev/null
+++ b/app/views/snippets/notes/_notes.html.haml
@@ -0,0 +1,2 @@
+%ul#notes-list.notes.main-notes-list.timeline
+ = render "projects/notes/notes"
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index 8a80013bbfd..98287cba5b4 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -3,7 +3,10 @@
= render 'shared/snippets/header'
%article.file-holder.snippet-file-content
- = render 'shared/snippets/blob', download_path: download_snippet_path(@snippet)
+ = render 'shared/snippets/blob'
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
+
+%ul#notes-list.notes.main-notes-list.timeline
+ #notes= render 'shared/notes/notes'
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index eb403c134d1..7b59e976492 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -8,7 +8,7 @@ class ExpireBuildInstanceArtifactsWorker
.reorder(nil)
.find_by(id: build_id)
- return unless build.try(:project)
+ return unless build&.project && !build.project.pending_delete
Rails.logger.info "Removing artifacts for build #{build.id}..."
build.erase_artifacts!
diff --git a/changelogs/unreleased/12910-personal-snippets-notes-show.yml b/changelogs/unreleased/12910-personal-snippets-notes-show.yml
new file mode 100644
index 00000000000..15c6f3c5e6a
--- /dev/null
+++ b/changelogs/unreleased/12910-personal-snippets-notes-show.yml
@@ -0,0 +1,4 @@
+---
+title: Display comments for personal snippets
+merge_request:
+author:
diff --git a/changelogs/unreleased/19364-webhook-edit.yml b/changelogs/unreleased/19364-webhook-edit.yml
new file mode 100644
index 00000000000..60e154b8b83
--- /dev/null
+++ b/changelogs/unreleased/19364-webhook-edit.yml
@@ -0,0 +1,4 @@
+---
+title: Implement ability to edit hooks
+merge_request: 10816
+author: Alexander Randa
diff --git a/changelogs/unreleased/26488-target-disabled-mr.yml b/changelogs/unreleased/26488-target-disabled-mr.yml
new file mode 100644
index 00000000000..02058481ccf
--- /dev/null
+++ b/changelogs/unreleased/26488-target-disabled-mr.yml
@@ -0,0 +1,4 @@
+---
+title: Disallow merge requests from fork when source project have disabled merge requests
+merge_request:
+author: mhasbini
diff --git a/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml
new file mode 100644
index 00000000000..6612cfd8866
--- /dev/null
+++ b/changelogs/unreleased/28968-prevent-people-from-creating-branches-if-they-don-have-permission-to-push.yml
@@ -0,0 +1,4 @@
+---
+title: Prevent people from creating branches if they don't have persmission to push
+merge_request:
+author:
diff --git a/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml b/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml
new file mode 100644
index 00000000000..56bce084546
--- /dev/null
+++ b/changelogs/unreleased/30272-bvl-reject-more-namespaces.yml
@@ -0,0 +1,4 @@
+---
+title: Improve validation of namespace & project paths
+merge_request: 10413
+author:
diff --git a/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml
new file mode 100644
index 00000000000..4452b13037b
--- /dev/null
+++ b/changelogs/unreleased/30535-display-whether-pages-is-enabled-in-the-admin-dashboard.yml
@@ -0,0 +1,4 @@
+---
+title: Display GitLab Pages status in Admin Dashboard
+merge_request:
+author:
diff --git a/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml
new file mode 100644
index 00000000000..950336ea932
--- /dev/null
+++ b/changelogs/unreleased/31254-change-git-commit-command-in-existing-folder.yml
@@ -0,0 +1,4 @@
+---
+title: Change Git commit command in Existing folder to git commit -m
+merge_request: 10900
+author: TM Lee
diff --git a/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml b/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml
new file mode 100644
index 00000000000..02c048cb3b4
--- /dev/null
+++ b/changelogs/unreleased/31560-workhose-gitaly-from-mirror.yml
@@ -0,0 +1,4 @@
+---
+title: rickettm Add repo parameter to gitaly:install and workhorse:install rake tasks
+merge_request: 10979
+author: M. Ricketts
diff --git a/changelogs/unreleased/add-tanuki-ci-status-favicons.yml b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml
new file mode 100644
index 00000000000..b60ad81947a
--- /dev/null
+++ b/changelogs/unreleased/add-tanuki-ci-status-favicons.yml
@@ -0,0 +1,4 @@
+---
+title: Updated CI status favicons to include the tanuki
+merge_request: 10923
+author:
diff --git a/changelogs/unreleased/dm-blob-download-button.yml b/changelogs/unreleased/dm-blob-download-button.yml
new file mode 100644
index 00000000000..bd31137b670
--- /dev/null
+++ b/changelogs/unreleased/dm-blob-download-button.yml
@@ -0,0 +1,4 @@
+---
+title: Show Raw button as Download for binary files
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-snippet-download-button.yml b/changelogs/unreleased/dm-snippet-download-button.yml
new file mode 100644
index 00000000000..09ece1e7f98
--- /dev/null
+++ b/changelogs/unreleased/dm-snippet-download-button.yml
@@ -0,0 +1,4 @@
+---
+title: Add download button to project snippets
+merge_request:
+author:
diff --git a/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml
new file mode 100644
index 00000000000..a4345b70744
--- /dev/null
+++ b/changelogs/unreleased/dont-blow-up-when-email-has-no-references-header.yml
@@ -0,0 +1,5 @@
+---
+title: Gracefully handle failures for incoming emails which do not match on the To
+ header, and have no References header
+merge_request:
+author:
diff --git a/config/initializers/active_record_query_trace.rb b/config/initializers/active_record_query_trace.rb
deleted file mode 100644
index 4b3c2803b3b..00000000000
--- a/config/initializers/active_record_query_trace.rb
+++ /dev/null
@@ -1,5 +0,0 @@
-if ENV['ENABLE_QUERY_TRACE']
- require 'active_record_query_trace'
-
- ActiveRecordQueryTrace.enabled = 'true'
-end
diff --git a/config/initializers/carrierwave.rb b/config/initializers/carrierwave.rb
index 1933afcbfb1..cd7df44351a 100644
--- a/config/initializers/carrierwave.rb
+++ b/config/initializers/carrierwave.rb
@@ -6,6 +6,8 @@ if File.exist?(aws_file)
AWS_CONFIG = YAML.load(File.read(aws_file))[Rails.env]
CarrierWave.configure do |config|
+ config.fog_provider = 'fog/aws'
+
config.fog_credentials = {
provider: 'AWS', # required
aws_access_key_id: AWS_CONFIG['access_key_id'], # required
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 52ba10604d4..48993420ed9 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -50,8 +50,10 @@ namespace :admin do
resources :deploy_keys, only: [:index, :new, :create, :destroy]
- resources :hooks, only: [:index, :create, :destroy] do
- get :test
+ resources :hooks, only: [:index, :create, :edit, :update, :destroy] do
+ member do
+ get :test
+ end
end
resources :broadcast_messages, only: [:index, :edit, :create, :update, :destroy] do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 115ae2324b3..894faeb6188 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -44,7 +44,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :snippets, concerns: :awardable, constraints: { id: /\d+/ } do
member do
- get 'raw'
+ get :raw
post :mark_as_spam
end
end
@@ -138,6 +138,8 @@ constraints(ProjectUrlConstrainer.new) do
collection do
get :folder, path: 'folders/*id', constraints: { format: /(html|json)/ }
end
+
+ resources :deployments, only: [:index]
end
resource :cycle_analytics, only: [:show]
@@ -185,7 +187,7 @@ constraints(ProjectUrlConstrainer.new) do
end
end
- resources :hooks, only: [:index, :create, :destroy], constraints: { id: /\d+/ } do
+ resources :hooks, only: [:index, :create, :edit, :update, :destroy], constraints: { id: /\d+/ } do
member do
get :test
end
diff --git a/config/routes/snippets.rb b/config/routes/snippets.rb
index 56534f677be..dae83734fe6 100644
--- a/config/routes/snippets.rb
+++ b/config/routes/snippets.rb
@@ -1,10 +1,17 @@
resources :snippets, concerns: :awardable do
member do
- get 'raw'
- get 'download'
+ get :raw
post :mark_as_spam
post :preview_markdown
end
+
+ scope module: :snippets do
+ resources :notes, only: [:index, :create, :destroy, :update], concerns: :awardable, constraints: { id: /\d+/ } do
+ member do
+ delete :delete_attachment
+ end
+ end
+ end
end
get '/s/:username', to: redirect('/u/%{username}/snippets'),
diff --git a/config/webpack.config.js b/config/webpack.config.js
index cb0a57a3a41..0ec9e48845e 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -78,6 +78,11 @@ var config = {
loader: 'raw-loader',
},
{
+ test: /\.gif$/,
+ loader: 'url-loader',
+ query: { mimetype: 'image/gif' },
+ },
+ {
test: /\.(worker\.js|pdf)$/,
exclude: /node_modules/,
loader: 'file-loader',
diff --git a/db/migrate/20170327091750_add_created_at_index_to_deployments.rb b/db/migrate/20170327091750_add_created_at_index_to_deployments.rb
new file mode 100644
index 00000000000..fd6ed499b80
--- /dev/null
+++ b/db/migrate/20170327091750_add_created_at_index_to_deployments.rb
@@ -0,0 +1,15 @@
+class AddCreatedAtIndexToDeployments < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :deployments, :created_at
+ end
+
+ def down
+ remove_concurrent_index :deployments, :created_at
+ end
+end
diff --git a/db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb b/db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb
new file mode 100644
index 00000000000..a23f83205f1
--- /dev/null
+++ b/db/post_migrate/20170412174900_rename_reserved_dynamic_paths.rb
@@ -0,0 +1,55 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class RenameReservedDynamicPaths < ActiveRecord::Migration
+ include Gitlab::Database::RenameReservedPathsMigration::V1
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ DISALLOWED_ROOT_PATHS = %w[
+ -
+ abuse_reports
+ api
+ autocomplete
+ explore
+ health_check
+ import
+ invites
+ jwt
+ koding
+ member
+ notification_settings
+ oauth
+ sent_notifications
+ unicorn_test
+ uploads
+ users
+ ]
+
+ DISALLOWED_WILDCARD_PATHS = %w[
+ environments/folders
+ gitlab-lfs/objects
+ info/lfs/objects
+ ]
+
+ DISSALLOWED_GROUP_PATHS = %w[
+ activity
+ avatar
+ group_members
+ labels
+ milestones
+ subgroups
+ ]
+
+ def up
+ rename_root_paths(DISALLOWED_ROOT_PATHS)
+ rename_wildcard_paths(DISALLOWED_WILDCARD_PATHS)
+ rename_child_paths(DISSALLOWED_GROUP_PATHS)
+ end
+
+ def down
+ # nothing to do
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index b938657a186..be6684f3a6b 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -386,6 +386,7 @@ ActiveRecord::Schema.define(version: 20170426181740) do
t.string "on_stop"
end
+ add_index "deployments", ["created_at"], name: "index_deployments_on_created_at", using: :btree
add_index "deployments", ["project_id", "environment_id", "iid"], name: "index_deployments_on_project_id_and_environment_id_and_iid", using: :btree
add_index "deployments", ["project_id", "iid"], name: "index_deployments_on_project_id_and_iid", unique: true, using: :btree
diff --git a/doc/administration/high_availability/load_balancer.md b/doc/administration/high_availability/load_balancer.md
index d9ca74ca1a3..359de0efadb 100644
--- a/doc/administration/high_availability/load_balancer.md
+++ b/doc/administration/high_availability/load_balancer.md
@@ -13,7 +13,7 @@ you need to use with GitLab.
| LB Port | Backend Port | Protocol |
| ------- | ------------ | --------------- |
| 80 | 80 | HTTP [^1] |
-| 443 | 443 | HTTPS [^1] [^2] |
+| 443 | 443 | TCP or HTTPS [^1] [^2] |
| 22 | 22 | TCP |
## GitLab Pages Ports
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 3b5ee86b68b..91e844c7b42 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -32,7 +32,7 @@ In brief:
As web terminals use WebSockets, every HTTP/HTTPS reverse proxy in front of
Workhorse needs to be configured to pass the `Connection` and `Upgrade` headers
-through to the next one in the chain. If you installed Gitlab using Omnibus, or
+through to the next one in the chain. If you installed GitLab using Omnibus, or
from source, starting with GitLab 8.15, this should be done by the default
configuration, so there's no need for you to do anything.
@@ -58,7 +58,7 @@ document for more details.
If you'd like to disable web terminal support in GitLab, just stop passing
the `Connection` and `Upgrade` hop-by-hop headers in the *first* HTTP reverse
proxy in the chain. For most users, this will be the NGINX server bundled with
-Omnibus Gitlab, in which case, you need to:
+Omnibus GitLab, in which case, you need to:
* Find the `nginx['proxy_set_headers']` section of your `gitlab.rb` file
* Ensure the whole block is uncommented, and then comment out or remove the
diff --git a/doc/development/migration_style_guide.md b/doc/development/migration_style_guide.md
index 3e8b709c18f..77ba2a5fd87 100644
--- a/doc/development/migration_style_guide.md
+++ b/doc/development/migration_style_guide.md
@@ -270,3 +270,28 @@ end
When doing so be sure to explicitly set the model's table name so it's not
derived from the class name or namespace.
+
+### Renaming reserved paths
+
+When a new route for projects is introduced that could conflict with any
+existing records. The path for this records should be renamed, and the
+related data should be moved on disk.
+
+Since we had to do this a few times already, there are now some helpers to help
+with this.
+
+To use this you can include `Gitlab::Database::RenameReservedPathsMigration::V1`
+in your migration. This will provide 3 methods which you can pass one or more
+paths that need to be rejected.
+
+**`rename_root_paths`**: This will rename the path of all _namespaces_ with the
+given name that don't have a `parent_id`.
+
+**`rename_child_paths`**: This will rename the path of all _namespaces_ with the
+given name that have a `parent_id`.
+
+**`rename_wildcard_paths`**: This will rename the path of all _projects_, and all
+_namespaces_ that have a `project_id`.
+
+The `path` column for these rows will be renamed to their previous value followed
+by an integer. For example: `users` would turn into `users0`
diff --git a/doc/gitlab-basics/create-project.md b/doc/gitlab-basics/create-project.md
index 1c549844ee1..2513f4b420a 100644
--- a/doc/gitlab-basics/create-project.md
+++ b/doc/gitlab-basics/create-project.md
@@ -1,24 +1,28 @@
# How to create a project in GitLab
-There are two ways to create a new project in GitLab.
-
-1. While in your dashboard, you can create a new project using the **New project**
- green button or you can use the cross icon in the upper right corner next to
- your avatar which is always visible.
+1. In your dashboard, click the green **New project** button or use the plus
+ icon in the upper right corner of the navigation bar.
![Create a project](img/create_new_project_button.png)
-1. From there you can see several options.
+1. This opens the **New project** page.
![Project information](img/create_new_project_info.png)
-1. Fill out the information:
-
- 1. "Project name" is the name of your project (you can't use special characters,
- but you can use spaces, hyphens, underscores or even emojis).
- 1. The "Project description" is optional and will be shown in your project's
- dashboard so others can briefly understand what your project is about.
- 1. Select a [visibility level](../public_access/public_access.md).
- 1. You can also [import your existing projects](../workflow/importing/README.md).
-
-1. Finally, click **Create project**.
+1. Provide the following information:
+ - Enter the name of your project in the **Project name** field. You can't use
+ special characters, but you can use spaces, hyphens, underscores or even
+ emoji.
+ - If you have a project in a different repository, you can [import it] by
+ clicking an **Import project from** button provided this is enabled in
+ your GitLab instance. Ask your administrator if not.
+ - The **Project description (optional)** field enables you to enter a
+ description for your project's dashboard, which will help others
+ understand what your project is about. Though it's not required, it's a good
+ idea to fill this in.
+ - Changing the **Visibility Level** modifies the project's
+ [viewing and access rights](../public_access/public_access.md) for users.
+
+1. Click **Create project**.
+
+[import it]: ../workflow/importing/README.md
diff --git a/doc/gitlab-basics/img/create_new_project_button.png b/doc/gitlab-basics/img/create_new_project_button.png
index 8d7a69e55ed..567f104880f 100644
--- a/doc/gitlab-basics/img/create_new_project_button.png
+++ b/doc/gitlab-basics/img/create_new_project_button.png
Binary files differ
diff --git a/doc/install/installation.md b/doc/install/installation.md
index b6bbc2a0af6..dc807d93bbb 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -423,6 +423,11 @@ which is the recommended location.
sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]" RAILS_ENV=production
+You can specify a different Git repository by providing it as an extra paramter:
+
+ sudo -u git -H bundle exec rake "gitlab:workhorse:install[/home/git/gitlab-workhorse,https://example.com/gitlab-workhorse.git]" RAILS_ENV=production
+
+
### Initialize Database and Activate Advanced Features
sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production
@@ -466,6 +471,12 @@ with setting up Gitaly until you upgrade to GitLab 9.2 or later.
# Fetch Gitaly source with Git and compile with Go
sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly]" RAILS_ENV=production
+You can specify a different Git repository by providing it as an extra paramter:
+
+ sudo -u git -H bundle exec rake "gitlab:gitaly:install[/home/git/gitaly,https://example.com/gitaly.git]" RAILS_ENV=production
+
+Next, make sure gitaly configured:
+
# Restrict Gitaly socket access
sudo chmod 0700 /home/git/gitlab/tmp/sockets/private
sudo chown git /home/git/gitlab/tmp/sockets/private
diff --git a/doc/update/8.10-to-8.11.md b/doc/update/8.10-to-8.11.md
index e5e3cd395df..e538983e603 100644
--- a/doc/update/8.10-to-8.11.md
+++ b/doc/update/8.10-to-8.11.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/8.11-to-8.12.md b/doc/update/8.11-to-8.12.md
index d6b3b0ffa5a..604166beb56 100644
--- a/doc/update/8.11-to-8.12.md
+++ b/doc/update/8.11-to-8.12.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/8.12-to-8.13.md b/doc/update/8.12-to-8.13.md
index ed0e668d854..d83965131f5 100644
--- a/doc/update/8.12-to-8.13.md
+++ b/doc/update/8.12-to-8.13.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/doc/update/8.13-to-8.14.md b/doc/update/8.13-to-8.14.md
index aa1c659717e..aaadcec8ac0 100644
--- a/doc/update/8.13-to-8.14.md
+++ b/doc/update/8.13-to-8.14.md
@@ -49,6 +49,8 @@ sudo gem install bundler --no-ri --no-rdoc
### 4. Get latest code
```bash
+cd /home/git/gitlab
+
sudo -u git -H git fetch --all
sudo -u git -H git checkout -- db/schema.rb # local changes will be restored automatically
```
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 36fe21a047c..ef09bddddd8 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -367,7 +367,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
step 'I should see buttons for allowed commands' do
page.within '.content' do
- expect(page).to have_link 'Open raw'
+ expect(page).to have_link 'Download'
expect(page).to have_content 'History'
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index e5793fbc5cb..710deba5ae3 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -20,6 +20,8 @@ module API
error!(errors[:validate_fork], 422)
elsif errors[:validate_branches].any?
conflict!(errors[:validate_branches])
+ elsif errors[:base].any?
+ error!(errors[:base], 422)
end
render_api_error!(errors, 400)
diff --git a/lib/api/v3/merge_requests.rb b/lib/api/v3/merge_requests.rb
index 3077240e650..1616142a619 100644
--- a/lib/api/v3/merge_requests.rb
+++ b/lib/api/v3/merge_requests.rb
@@ -23,6 +23,8 @@ module API
error!(errors[:validate_fork], 422)
elsif errors[:validate_branches].any?
conflict!(errors[:validate_branches])
+ elsif errors[:base].any?
+ error!(errors[:base], 422)
end
render_api_error!(errors, 400)
diff --git a/lib/constraints/group_url_constrainer.rb b/lib/constraints/group_url_constrainer.rb
index bae4db1ca4d..1501f64d537 100644
--- a/lib/constraints/group_url_constrainer.rb
+++ b/lib/constraints/group_url_constrainer.rb
@@ -2,16 +2,8 @@ class GroupUrlConstrainer
def matches?(request)
id = request.params[:id]
- return false unless valid?(id)
+ return false unless DynamicPathValidator.valid?(id)
Group.find_by_full_path(id).present?
end
-
- private
-
- def valid?(id)
- id.split('/').all? do |namespace|
- NamespaceValidator.valid?(namespace)
- end
- end
end
diff --git a/lib/constraints/project_url_constrainer.rb b/lib/constraints/project_url_constrainer.rb
index a10b4657d7d..d0ce2caffff 100644
--- a/lib/constraints/project_url_constrainer.rb
+++ b/lib/constraints/project_url_constrainer.rb
@@ -4,9 +4,7 @@ class ProjectUrlConstrainer
project_path = request.params[:project_id] || request.params[:id]
full_path = namespace_path + '/' + project_path
- unless ProjectPathValidator.valid?(project_path)
- return false
- end
+ return false unless DynamicPathValidator.valid?(full_path)
Project.find_by_full_path(full_path).present?
end
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 6dabbe0264c..298b1a1f4e6 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -498,6 +498,29 @@ module Gitlab
columns(table).find { |column| column.name == name }
end
+
+ # This will replace the first occurance of a string in a column with
+ # the replacement
+ # On postgresql we can use `regexp_replace` for that.
+ # On mysql we find the location of the pattern, and overwrite it
+ # with the replacement
+ def replace_sql(column, pattern, replacement)
+ quoted_pattern = Arel::Nodes::Quoted.new(pattern.to_s)
+ quoted_replacement = Arel::Nodes::Quoted.new(replacement.to_s)
+
+ if Database.mysql?
+ locate = Arel::Nodes::NamedFunction.
+ new('locate', [quoted_pattern, column])
+ insert_in_place = Arel::Nodes::NamedFunction.
+ new('insert', [column, locate, pattern.size, quoted_replacement])
+
+ Arel::Nodes::SqlLiteral.new(insert_in_place.to_sql)
+ else
+ replace = Arel::Nodes::NamedFunction.
+ new("regexp_replace", [column, quoted_pattern, quoted_replacement])
+ Arel::Nodes::SqlLiteral.new(replace.to_sql)
+ end
+ end
end
end
end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb
new file mode 100644
index 00000000000..89530082cd2
--- /dev/null
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1.rb
@@ -0,0 +1,35 @@
+# This module can be included in migrations to make it easier to rename paths
+# of `Namespace` & `Project` models certain paths would become `reserved`.
+#
+# If the way things are stored on the filesystem related to namespaces and
+# projects ever changes. Don't update this module, or anything nested in `V1`,
+# since it needs to keep functioning for all migrations using it using the state
+# that the data is in at the time. Instead, create a `V2` module that implements
+# the new way of reserving paths.
+module Gitlab
+ module Database
+ module RenameReservedPathsMigration
+ module V1
+ def self.included(kls)
+ kls.include(MigrationHelpers)
+ end
+
+ def rename_wildcard_paths(one_or_more_paths)
+ rename_child_paths(one_or_more_paths)
+ paths = Array(one_or_more_paths)
+ RenameProjects.new(paths, self).rename_projects
+ end
+
+ def rename_child_paths(one_or_more_paths)
+ paths = Array(one_or_more_paths)
+ RenameNamespaces.new(paths, self).rename_namespaces(type: :child)
+ end
+
+ def rename_root_paths(paths)
+ paths = Array(paths)
+ RenameNamespaces.new(paths, self).rename_namespaces(type: :top_level)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
new file mode 100644
index 00000000000..4fdcb682c2f
--- /dev/null
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/migration_classes.rb
@@ -0,0 +1,76 @@
+module Gitlab
+ module Database
+ module RenameReservedPathsMigration
+ module V1
+ module MigrationClasses
+ module Routable
+ def full_path
+ if route && route.path.present?
+ @full_path ||= route.path
+ else
+ update_route if persisted?
+
+ build_full_path
+ end
+ end
+
+ def build_full_path
+ if parent && path
+ parent.full_path + '/' + path
+ else
+ path
+ end
+ end
+
+ def update_route
+ prepare_route
+ route.save
+ end
+
+ def prepare_route
+ route || build_route(source: self)
+ route.path = build_full_path
+ @full_path = nil
+ end
+ end
+
+ class Namespace < ActiveRecord::Base
+ include MigrationClasses::Routable
+ self.table_name = 'namespaces'
+ belongs_to :parent,
+ class_name: "#{MigrationClasses.name}::Namespace"
+ has_one :route, as: :source
+ has_many :children,
+ class_name: "#{MigrationClasses.name}::Namespace",
+ foreign_key: :parent_id
+
+ # Overridden to have the correct `source_type` for the `route` relation
+ def self.name
+ 'Namespace'
+ end
+ end
+
+ class Route < ActiveRecord::Base
+ self.table_name = 'routes'
+ belongs_to :source, polymorphic: true
+ end
+
+ class Project < ActiveRecord::Base
+ include MigrationClasses::Routable
+ has_one :route, as: :source
+ self.table_name = 'projects'
+
+ def repository_storage_path
+ Gitlab.config.repositories.storages[repository_storage]['path']
+ end
+
+ # Overridden to have the correct `source_type` for the `route` relation
+ def self.name
+ 'Project'
+ end
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
new file mode 100644
index 00000000000..de4e6e7c404
--- /dev/null
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base.rb
@@ -0,0 +1,131 @@
+module Gitlab
+ module Database
+ module RenameReservedPathsMigration
+ module V1
+ class RenameBase
+ attr_reader :paths, :migration
+
+ delegate :update_column_in_batches,
+ :replace_sql,
+ to: :migration
+
+ def initialize(paths, migration)
+ @paths = paths
+ @migration = migration
+ end
+
+ def path_patterns
+ @path_patterns ||= paths.map { |path| "%#{path}" }
+ end
+
+ def rename_path_for_routable(routable)
+ old_path = routable.path
+ old_full_path = routable.full_path
+ # Only remove the last occurrence of the path name to get the parent namespace path
+ namespace_path = remove_last_occurrence(old_full_path, old_path)
+ new_path = rename_path(namespace_path, old_path)
+ new_full_path = join_routable_path(namespace_path, new_path)
+
+ # skips callbacks & validations
+ routable.class.where(id: routable).
+ update_all(path: new_path)
+
+ rename_routes(old_full_path, new_full_path)
+
+ [old_full_path, new_full_path]
+ end
+
+ def rename_routes(old_full_path, new_full_path)
+ replace_statement = replace_sql(Route.arel_table[:path],
+ old_full_path,
+ new_full_path)
+
+ update_column_in_batches(:routes, :path, replace_statement) do |table, query|
+ query.where(MigrationClasses::Route.arel_table[:path].matches("#{old_full_path}%"))
+ end
+ end
+
+ def rename_path(namespace_path, path_was)
+ counter = 0
+ path = "#{path_was}#{counter}"
+
+ while route_exists?(join_routable_path(namespace_path, path))
+ counter += 1
+ path = "#{path_was}#{counter}"
+ end
+
+ path
+ end
+
+ def remove_last_occurrence(string, pattern)
+ string.reverse.sub(pattern.reverse, "").reverse
+ end
+
+ def join_routable_path(namespace_path, top_level)
+ if namespace_path.present?
+ File.join(namespace_path, top_level)
+ else
+ top_level
+ end
+ end
+
+ def route_exists?(full_path)
+ MigrationClasses::Route.where(Route.arel_table[:path].matches(full_path)).any?
+ end
+
+ def move_pages(old_path, new_path)
+ move_folders(pages_dir, old_path, new_path)
+ end
+
+ def move_uploads(old_path, new_path)
+ return unless file_storage?
+
+ move_folders(uploads_dir, old_path, new_path)
+ end
+
+ def move_folders(directory, old_relative_path, new_relative_path)
+ old_path = File.join(directory, old_relative_path)
+ return unless File.directory?(old_path)
+
+ new_path = File.join(directory, new_relative_path)
+ FileUtils.mv(old_path, new_path)
+ end
+
+ def remove_cached_html_for_projects(project_ids)
+ update_column_in_batches(:projects, :description_html, nil) do |table, query|
+ query.where(table[:id].in(project_ids))
+ end
+
+ update_column_in_batches(:issues, :description_html, nil) do |table, query|
+ query.where(table[:project_id].in(project_ids))
+ end
+
+ update_column_in_batches(:merge_requests, :description_html, nil) do |table, query|
+ query.where(table[:target_project_id].in(project_ids))
+ end
+
+ update_column_in_batches(:notes, :note_html, nil) do |table, query|
+ query.where(table[:project_id].in(project_ids))
+ end
+
+ update_column_in_batches(:milestones, :description_html, nil) do |table, query|
+ query.where(table[:project_id].in(project_ids))
+ end
+ end
+
+ def file_storage?
+ CarrierWave::Uploader::Base.storage == CarrierWave::Storage::File
+ end
+
+ def uploads_dir
+ File.join(CarrierWave.root, "uploads")
+ end
+
+ def pages_dir
+ Settings.pages.path
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
new file mode 100644
index 00000000000..b9f4f3cff3c
--- /dev/null
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces.rb
@@ -0,0 +1,72 @@
+module Gitlab
+ module Database
+ module RenameReservedPathsMigration
+ module V1
+ class RenameNamespaces < RenameBase
+ include Gitlab::ShellAdapter
+
+ def rename_namespaces(type:)
+ namespaces_for_paths(type: type).each do |namespace|
+ rename_namespace(namespace)
+ end
+ end
+
+ def namespaces_for_paths(type:)
+ namespaces = case type
+ when :child
+ MigrationClasses::Namespace.where.not(parent_id: nil)
+ when :top_level
+ MigrationClasses::Namespace.where(parent_id: nil)
+ end
+ with_paths = MigrationClasses::Route.arel_table[:path].
+ matches_any(path_patterns)
+ namespaces.joins(:route).where(with_paths)
+ end
+
+ def rename_namespace(namespace)
+ old_full_path, new_full_path = rename_path_for_routable(namespace)
+
+ move_repositories(namespace, old_full_path, new_full_path)
+ move_uploads(old_full_path, new_full_path)
+ move_pages(old_full_path, new_full_path)
+ remove_cached_html_for_projects(projects_for_namespace(namespace).map(&:id))
+ end
+
+ def move_repositories(namespace, old_full_path, new_full_path)
+ repo_paths_for_namespace(namespace).each do |repository_storage_path|
+ # Ensure old directory exists before moving it
+ gitlab_shell.add_namespace(repository_storage_path, old_full_path)
+
+ unless gitlab_shell.mv_namespace(repository_storage_path, old_full_path, new_full_path)
+ message = "Exception moving path #{repository_storage_path} \
+ from #{old_full_path} to #{new_full_path}"
+ Rails.logger.error message
+ end
+ end
+ end
+
+ def repo_paths_for_namespace(namespace)
+ projects_for_namespace(namespace).distinct.select(:repository_storage).
+ map(&:repository_storage_path)
+ end
+
+ def projects_for_namespace(namespace)
+ namespace_ids = child_ids_for_parent(namespace, ids: [namespace.id])
+ namespace_or_children = MigrationClasses::Project.
+ arel_table[:namespace_id].
+ in(namespace_ids)
+ MigrationClasses::Project.where(namespace_or_children)
+ end
+
+ def child_ids_for_parent(namespace, ids: [])
+ namespace.children.each do |child|
+ ids << child.id
+ child_ids_for_parent(child, ids: ids) if child.children.any?
+ end
+ ids
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
new file mode 100644
index 00000000000..448717eb744
--- /dev/null
+++ b/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects.rb
@@ -0,0 +1,45 @@
+module Gitlab
+ module Database
+ module RenameReservedPathsMigration
+ module V1
+ class RenameProjects < RenameBase
+ include Gitlab::ShellAdapter
+
+ def rename_projects
+ projects_for_paths.each do |project|
+ rename_project(project)
+ end
+
+ remove_cached_html_for_projects(projects_for_paths.map(&:id))
+ end
+
+ def rename_project(project)
+ old_full_path, new_full_path = rename_path_for_routable(project)
+
+ move_repository(project, old_full_path, new_full_path)
+ move_repository(project, "#{old_full_path}.wiki", "#{new_full_path}.wiki")
+ move_uploads(old_full_path, new_full_path)
+ move_pages(old_full_path, new_full_path)
+ end
+
+ def move_repository(project, old_path, new_path)
+ unless gitlab_shell.mv_repository(project.repository_storage_path,
+ old_path,
+ new_path)
+ Rails.logger.error "Error moving #{old_path} to #{new_path}"
+ end
+ end
+
+ def projects_for_paths
+ return @projects_for_paths if @projects_for_paths
+
+ with_paths = MigrationClasses::Route.arel_table[:path]
+ .matches_any(path_patterns)
+
+ @projects_for_paths = MigrationClasses::Project.joins(:route).where(with_paths)
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/email/receiver.rb b/lib/gitlab/email/receiver.rb
index 419d56a51e0..c270c0ea9ff 100644
--- a/lib/gitlab/email/receiver.rb
+++ b/lib/gitlab/email/receiver.rb
@@ -70,6 +70,8 @@ module Gitlab
# Handle emails from clients which append with commas,
# example clients are Microsoft exchange and iOS app
Gitlab::IncomingEmail.scan_fallback_references(references)
+ when nil
+ []
end
end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index f6e4f279c06..aac210f19e8 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -2,31 +2,39 @@ module Gitlab
module EtagCaching
class Router
Route = Struct.new(:regexp, :name)
-
- RESERVED_WORDS = NamespaceValidator::WILDCARD_ROUTES.map { |word| "/#{word}/" }.join('|')
+ # We enable an ETag for every request matching the regex.
+ # To match a regex the path needs to match the following:
+ # - Don't contain a reserved word (expect for the words used in the
+ # regex itself)
+ # - Ending in `noteable/issue/<id>/notes` for the `issue_notes` route
+ # - Ending in `issues/id`/rendered_title` for the `issue_title` route
+ USED_IN_ROUTES = %w[noteable issue notes issues rendered_title
+ commit pipelines merge_requests new].freeze
+ RESERVED_WORDS = DynamicPathValidator::WILDCARD_ROUTES - USED_IN_ROUTES
+ RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS)
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS})).*/noteable/issue/\d+/notes\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
'issue_notes'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS})).*/issues/\d+/rendered_title\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/issues/\d+/rendered_title\z),
'issue_title'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS})).*/commit/\S+/pipelines\.json\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/commit/\S+/pipelines\.json\z),
'commit_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/new\.json\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/new\.json\z),
'new_merge_request_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS})).*/merge_requests/\d+/pipelines\.json\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/merge_requests/\d+/pipelines\.json\z),
'merge_request_pipelines'
),
Gitlab::EtagCaching::Router::Route.new(
- %r(^(?!.*(#{RESERVED_WORDS})).*/pipelines\.json\z),
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/pipelines\.json\z),
'project_pipelines'
)
].freeze
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index 18eda0279f7..c3f0de76d01 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -122,13 +122,30 @@ module Gitlab
# Returns the number of valid branches
def branch_count
- rugged.branches.count do |ref|
- begin
- ref.name && ref.target # ensures the branch is valid
+ Gitlab::GitalyClient.migrate(:branch_names) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.count_branch_names
+ else
+ rugged.branches.count do |ref|
+ begin
+ ref.name && ref.target # ensures the branch is valid
- true
- rescue Rugged::ReferenceError
- false
+ true
+ rescue Rugged::ReferenceError
+ false
+ end
+ end
+ end
+ end
+ end
+
+ # Returns the number of valid tags
+ def tag_count
+ Gitlab::GitalyClient.migrate(:tag_names) do |is_enabled|
+ if is_enabled
+ gitaly_ref_client.count_tag_names
+ else
+ rugged.tags.count
end
end
end
diff --git a/lib/gitlab/gitaly_client/ref.rb b/lib/gitlab/gitaly_client/ref.rb
index d3c0743db4e..2a5e8f73e55 100644
--- a/lib/gitlab/gitaly_client/ref.rb
+++ b/lib/gitlab/gitaly_client/ref.rb
@@ -34,6 +34,14 @@ module Gitlab
stub.find_ref_name(request).name
end
+ def count_tag_names
+ tag_names.count
+ end
+
+ def count_branch_names
+ branch_names.count
+ end
+
private
def consume_refs_response(response, prefix:)
diff --git a/lib/gitlab/regex.rb b/lib/gitlab/regex.rb
index 08b061d5e31..b7fef5dd068 100644
--- a/lib/gitlab/regex.rb
+++ b/lib/gitlab/regex.rb
@@ -22,6 +22,10 @@ module Gitlab
@namespace_regex ||= /\A#{NAMESPACE_REGEX_STR}\z/.freeze
end
+ def full_namespace_regex
+ @full_namespace_regex ||= %r{\A#{FULL_NAMESPACE_REGEX_STR}\z}
+ end
+
def namespace_route_regex
@namespace_route_regex ||= /#{NAMESPACE_REGEX_STR}/.freeze
end
diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb
index 54728e5ff0e..e46ff313654 100644
--- a/lib/gitlab/user_access.rb
+++ b/lib/gitlab/user_access.rb
@@ -44,9 +44,7 @@ module Gitlab
if ProtectedBranch.protected?(project, ref)
return true if project.empty_repo? && project.user_can_push_to_empty_repo?(user)
- has_access = project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
-
- has_access || !project.repository.branch_exists?(ref) && can_merge_to_branch?(ref)
+ project.protected_branches.protected_ref_accessible_to?(ref, user, action: :push)
else
user.can?(:push_code, project)
end
diff --git a/lib/tasks/gitlab/gitaly.rake b/lib/tasks/gitlab/gitaly.rake
index 046780481ba..3c5bc0146a1 100644
--- a/lib/tasks/gitlab/gitaly.rake
+++ b/lib/tasks/gitlab/gitaly.rake
@@ -1,18 +1,18 @@
namespace :gitlab do
namespace :gitaly do
desc "GitLab | Install or upgrade gitaly"
- task :install, [:dir] => :environment do |t, args|
+ task :install, [:dir, :repo] => :environment do |t, args|
require 'toml'
warn_user_is_not_gitlab
unless args.dir.present?
abort %(Please specify the directory where you want to install gitaly:\n rake "gitlab:gitaly:install[/home/git/gitaly]")
end
+ args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitaly.git')
version = Gitlab::GitalyClient.expected_server_version
- repo = 'https://gitlab.com/gitlab-org/gitaly.git'
- checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
+ checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
_, status = Gitlab::Popen.popen(%w[which gmake])
command = status.zero? ? 'gmake' : 'make'
diff --git a/lib/tasks/gitlab/workhorse.rake b/lib/tasks/gitlab/workhorse.rake
index a00b02188cf..e7ac0b5859f 100644
--- a/lib/tasks/gitlab/workhorse.rake
+++ b/lib/tasks/gitlab/workhorse.rake
@@ -1,16 +1,16 @@
namespace :gitlab do
namespace :workhorse do
desc "GitLab | Install or upgrade gitlab-workhorse"
- task :install, [:dir] => :environment do |t, args|
+ task :install, [:dir, :repo] => :environment do |t, args|
warn_user_is_not_gitlab
unless args.dir.present?
abort %(Please specify the directory where you want to install gitlab-workhorse:\n rake "gitlab:workhorse:install[/home/git/gitlab-workhorse]")
end
+ args.with_defaults(repo: 'https://gitlab.com/gitlab-org/gitlab-workhorse.git')
version = Gitlab::Workhorse.version
- repo = 'https://gitlab.com/gitlab-org/gitlab-workhorse.git'
- checkout_or_clone_version(version: version, repo: repo, target_dir: args.dir)
+ checkout_or_clone_version(version: version, repo: args.repo, target_dir: args.dir)
_, status = Gitlab::Popen.popen(%w[which gmake])
command = status.zero? ? 'gmake' : 'make'
diff --git a/package.json b/package.json
index f8c151ebd81..9ed5e1a7475 100644
--- a/package.json
+++ b/package.json
@@ -34,6 +34,7 @@
"jszip-utils": "^0.0.2",
"marked": "^0.3.6",
"mousetrap": "^1.4.6",
+ "pdfjs-dist": "^1.8.252",
"pikaday": "^1.5.1",
"prismjs": "^1.6.0",
"raphael": "^2.2.7",
@@ -46,6 +47,7 @@
"three-stl-loader": "^1.0.4",
"timeago.js": "^2.0.5",
"underscore": "^1.8.3",
+ "url-loader": "^0.5.8",
"visibilityjs": "^1.2.4",
"vue": "^2.2.6",
"vue-loader": "^11.3.4",
diff --git a/scripts/lint-doc.sh b/scripts/lint-doc.sh
index 62236ed539a..54c1ef3dfdd 100755
--- a/scripts/lint-doc.sh
+++ b/scripts/lint-doc.sh
@@ -21,4 +21,3 @@ fi
echo "✔ Linting passed"
exit 0
-
diff --git a/scripts/notify_slack.sh b/scripts/notify_slack.sh
deleted file mode 100755
index 6b3bc563c7a..00000000000
--- a/scripts/notify_slack.sh
+++ /dev/null
@@ -1,13 +0,0 @@
-#!/bin/sh
-# Sends Slack notification ERROR_MSG to CHANNEL
-# An env. variable CI_SLACK_WEBHOOK_URL needs to be set.
-
-CHANNEL=$1
-ERROR_MSG=$2
-
-if [ -z "$CHANNEL" ] || [ -z "$ERROR_MSG" ] || [ -z "$CI_SLACK_WEBHOOK_URL" ]; then
- echo "Missing argument(s) - Use: $0 channel message"
- echo "and set CI_SLACK_WEBHOOK_URL environment variable."
-else
- curl -X POST --data-urlencode 'payload={"channel": "'"$CHANNEL"'", "username": "gitlab-ci", "text": "'"$ERROR_MSG"'", "icon_emoji": ":gitlab:"}' "$CI_SLACK_WEBHOOK_URL"
-fi \ No newline at end of file
diff --git a/scripts/static-analysis b/scripts/static-analysis
new file mode 100755
index 00000000000..192d9d4c3ba
--- /dev/null
+++ b/scripts/static-analysis
@@ -0,0 +1,40 @@
+#!/usr/bin/env ruby
+
+require ::File.expand_path('../lib/gitlab/popen', __dir__)
+
+tasks = [
+ %w[bundle exec rake config_lint],
+ %w[bundle exec rake flay],
+ %w[bundle exec rake haml_lint],
+ %w[bundle exec rake scss_lint],
+ %w[bundle exec rake brakeman],
+ %w[bundle exec license_finder],
+ %w[scripts/lint-doc.sh],
+ %w[yarn run eslint],
+ %w[bundle exec rubocop --require rubocop-rspec]
+]
+
+failed_tasks = tasks.reduce({}) do |failures, task|
+ output, status = Gitlab::Popen.popen(task)
+
+ puts "Running: #{task.join(' ')}"
+ puts output
+
+ failures[task.join(' ')] = output unless status.zero?
+
+ failures
+end
+
+if failed_tasks.empty?
+ puts 'All static analyses passed successfully.'
+else
+ puts "\n===================================================\n\n"
+ puts "Some static analyses failed:"
+
+ failed_tasks.each do |failed_task, output|
+ puts "\n**** #{failed_task} failed with the following error:\n\n"
+ puts output
+ end
+
+ exit 1
+end
diff --git a/spec/controllers/projects/deployments_controller_spec.rb b/spec/controllers/projects/deployments_controller_spec.rb
new file mode 100644
index 00000000000..89692b601b2
--- /dev/null
+++ b/spec/controllers/projects/deployments_controller_spec.rb
@@ -0,0 +1,42 @@
+require 'spec_helper'
+
+describe Projects::DeploymentsController do
+ include ApiHelpers
+
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ before do
+ project.add_master(user)
+
+ sign_in(user)
+ end
+
+ describe 'GET #index' do
+ it 'returns list of deployments from last 8 hours' do
+ create(:deployment, environment: environment, created_at: 9.hours.ago)
+ create(:deployment, environment: environment, created_at: 7.hours.ago)
+ create(:deployment, environment: environment)
+
+ get :index, environment_params(after: 8.hours.ago)
+
+ expect(response).to be_ok
+
+ expect(json_response['deployments'].count).to eq(2)
+ end
+
+ it 'returns a list with deployments information' do
+ create(:deployment, environment: environment)
+
+ get :index, environment_params
+
+ expect(response).to be_ok
+ expect(response).to match_response_schema('deployments')
+ end
+ end
+
+ def environment_params(opts = {})
+ opts.reverse_merge(namespace_id: project.namespace, project_id: project, environment_id: environment.id)
+ end
+end
diff --git a/spec/controllers/projects/notes_controller_spec.rb b/spec/controllers/projects/notes_controller_spec.rb
index f140eaef5d5..45f4cf9180d 100644
--- a/spec/controllers/projects/notes_controller_spec.rb
+++ b/spec/controllers/projects/notes_controller_spec.rb
@@ -167,6 +167,47 @@ describe Projects::NotesController do
end
end
+ describe 'DELETE destroy' do
+ let(:request_params) do
+ {
+ namespace_id: project.namespace,
+ project_id: project,
+ id: note,
+ format: :js
+ }
+ end
+
+ context 'user is the author of a note' do
+ before do
+ sign_in(note.author)
+ project.team << [note.author, :developer]
+ end
+
+ it "returns status 200 for html" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "deletes the note" do
+ expect { delete :destroy, request_params }.to change { Note.count }.from(1).to(0)
+ end
+ end
+
+ context 'user is not the author of a note' do
+ before do
+ sign_in(user)
+ project.team << [user, :developer]
+ end
+
+ it "returns status 404" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(404)
+ end
+ end
+ end
+
describe 'POST toggle_award_emoji' do
before do
sign_in(user)
diff --git a/spec/controllers/snippets/notes_controller_spec.rb b/spec/controllers/snippets/notes_controller_spec.rb
new file mode 100644
index 00000000000..1c494b8c7ab
--- /dev/null
+++ b/spec/controllers/snippets/notes_controller_spec.rb
@@ -0,0 +1,196 @@
+require 'spec_helper'
+
+describe Snippets::NotesController do
+ let(:user) { create(:user) }
+
+ let(:private_snippet) { create(:personal_snippet, :private) }
+ let(:internal_snippet) { create(:personal_snippet, :internal) }
+ let(:public_snippet) { create(:personal_snippet, :public) }
+
+ let(:note_on_private) { create(:note_on_personal_snippet, noteable: private_snippet) }
+ let(:note_on_internal) { create(:note_on_personal_snippet, noteable: internal_snippet) }
+ let(:note_on_public) { create(:note_on_personal_snippet, noteable: public_snippet) }
+
+ describe 'GET index' do
+ context 'when a snippet is public' do
+ before do
+ note_on_public
+
+ get :index, { snippet_id: public_snippet }
+ end
+
+ it "returns status 200" do
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns not empty array of notes" do
+ expect(JSON.parse(response.body)["notes"].empty?).to be_falsey
+ end
+ end
+
+ context 'when a snippet is internal' do
+ before do
+ note_on_internal
+ end
+
+ context 'when user not logged in' do
+ it "returns status 404" do
+ get :index, { snippet_id: internal_snippet }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user logged in' do
+ before do
+ sign_in(user)
+ end
+
+ it "returns status 200" do
+ get :index, { snippet_id: internal_snippet }
+
+ expect(response).to have_http_status(200)
+ end
+ end
+ end
+
+ context 'when a snippet is private' do
+ before do
+ note_on_private
+ end
+
+ context 'when user not logged in' do
+ it "returns status 404" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when user other than author logged in' do
+ before do
+ sign_in(user)
+ end
+
+ it "returns status 404" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(response).to have_http_status(404)
+ end
+ end
+
+ context 'when author logged in' do
+ before do
+ note_on_private
+
+ sign_in(private_snippet.author)
+ end
+
+ it "returns status 200" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "returns 1 note" do
+ get :index, { snippet_id: private_snippet }
+
+ expect(JSON.parse(response.body)['notes'].count).to eq(1)
+ end
+ end
+ end
+
+ context 'dont show non visible notes' do
+ before do
+ note_on_public
+
+ sign_in(user)
+
+ expect_any_instance_of(Note).to receive(:cross_reference_not_visible_for?).and_return(true)
+ end
+
+ it "does not return any note" do
+ get :index, { snippet_id: public_snippet }
+
+ expect(JSON.parse(response.body)['notes'].count).to eq(0)
+ end
+ end
+ end
+
+ describe 'DELETE destroy' do
+ let(:request_params) do
+ {
+ snippet_id: public_snippet,
+ id: note_on_public,
+ format: :js
+ }
+ end
+
+ context 'when user is the author of a note' do
+ before do
+ sign_in(note_on_public.author)
+ end
+
+ it "returns status 200" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "deletes the note" do
+ expect{ delete :destroy, request_params }.to change{ Note.count }.from(1).to(0)
+ end
+
+ context 'system note' do
+ before do
+ expect_any_instance_of(Note).to receive(:system?).and_return(true)
+ end
+
+ it "does not delete the note" do
+ expect{ delete :destroy, request_params }.not_to change{ Note.count }
+ end
+ end
+ end
+
+ context 'when user is not the author of a note' do
+ before do
+ sign_in(user)
+
+ note_on_public
+ end
+
+ it "returns status 404" do
+ delete :destroy, request_params
+
+ expect(response).to have_http_status(404)
+ end
+
+ it "does not update the note" do
+ expect{ delete :destroy, request_params }.not_to change{ Note.count }
+ end
+ end
+ end
+
+ describe 'POST toggle_award_emoji' do
+ let(:note) { create(:note_on_personal_snippet, noteable: public_snippet) }
+ before do
+ sign_in(user)
+ end
+
+ subject { post(:toggle_award_emoji, snippet_id: public_snippet, id: note.id, name: "thumbsup") }
+
+ it "toggles the award emoji" do
+ expect { subject }.to change { note.award_emoji.count }.by(1)
+
+ expect(response).to have_http_status(200)
+ end
+
+ it "removes the already awarded emoji when it exists" do
+ note.toggle_award_emoji('thumbsup', user) # create award emoji before
+
+ expect { subject }.to change { AwardEmoji.count }.by(-1)
+
+ expect(response).to have_http_status(200)
+ end
+ end
+end
diff --git a/spec/controllers/snippets_controller_spec.rb b/spec/controllers/snippets_controller_spec.rb
index 234f3edd3d8..41cd5bdcdd8 100644
--- a/spec/controllers/snippets_controller_spec.rb
+++ b/spec/controllers/snippets_controller_spec.rb
@@ -350,144 +350,138 @@ describe SnippetsController do
end
end
- %w(raw download).each do |action|
- describe "GET #{action}" do
- context 'when the personal snippet is private' do
- let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
+ describe "GET #raw" do
+ context 'when the personal snippet is private' do
+ let(:personal_snippet) { create(:personal_snippet, :private, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- context 'when signed in user is not the author' do
- let(:other_author) { create(:author) }
- let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
+ context 'when signed in user is not the author' do
+ let(:other_author) { create(:author) }
+ let(:other_personal_snippet) { create(:personal_snippet, :private, author: other_author) }
- it 'responds with status 404' do
- get action, id: other_personal_snippet.to_param
+ it 'responds with status 404' do
+ get :raw, id: other_personal_snippet.to_param
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'when signed in user is the author' do
- before { get action, id: personal_snippet.to_param }
+ context 'when signed in user is the author' do
+ before { get :raw, id: personal_snippet.to_param }
- it 'responds with status 200' do
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ it 'responds with status 200' do
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- it 'has expected headers' do
- expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
+ it 'has expected headers' do
+ expect(response.header['Content-Type']).to eq('text/plain; charset=utf-8')
- if action == :download
- expect(response.header['Content-Disposition']).to match(/attachment/)
- elsif action == :raw
- expect(response.header['Content-Disposition']).to match(/inline/)
- end
- end
+ expect(response.header['Content-Disposition']).to match(/inline/)
end
end
+ end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
+ end
- context 'when the personal snippet is internal' do
- let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
+ context 'when the personal snippet is internal' do
+ let(:personal_snippet) { create(:personal_snippet, :internal, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get :raw, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
+ end
- context 'when not signed in' do
- it 'redirects to the sign in page' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'redirects to the sign in page' do
+ get :raw, id: personal_snippet.to_param
- expect(response).to redirect_to(new_user_session_path)
- end
+ expect(response).to redirect_to(new_user_session_path)
end
end
+ end
- context 'when the personal snippet is public' do
- let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
+ context 'when the personal snippet is public' do
+ let(:personal_snippet) { create(:personal_snippet, :public, author: user) }
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ it 'responds with status 200' do
+ get :raw, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
+ end
- context 'CRLF line ending' do
- let(:personal_snippet) do
- create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
- end
+ context 'CRLF line ending' do
+ let(:personal_snippet) do
+ create(:personal_snippet, :public, author: user, content: "first line\r\nsecond line\r\nthird line")
+ end
- it 'returns LF line endings by default' do
- get action, id: personal_snippet.to_param
+ it 'returns LF line endings by default' do
+ get :raw, id: personal_snippet.to_param
- expect(response.body).to eq("first line\nsecond line\nthird line")
- end
+ expect(response.body).to eq("first line\nsecond line\nthird line")
+ end
- it 'does not convert line endings when parameter present' do
- get action, id: personal_snippet.to_param, line_ending: :raw
+ it 'does not convert line endings when parameter present' do
+ get :raw, id: personal_snippet.to_param, line_ending: :raw
- expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
- end
+ expect(response.body).to eq("first line\r\nsecond line\r\nthird line")
end
end
+ end
- context 'when not signed in' do
- it 'responds with status 200' do
- get action, id: personal_snippet.to_param
+ context 'when not signed in' do
+ it 'responds with status 200' do
+ get :raw, id: personal_snippet.to_param
- expect(assigns(:snippet)).to eq(personal_snippet)
- expect(response).to have_http_status(200)
- end
+ expect(assigns(:snippet)).to eq(personal_snippet)
+ expect(response).to have_http_status(200)
end
end
+ end
- context 'when the personal snippet does not exist' do
- context 'when signed in' do
- before do
- sign_in(user)
- end
+ context 'when the personal snippet does not exist' do
+ context 'when signed in' do
+ before do
+ sign_in(user)
+ end
- it 'responds with status 404' do
- get action, id: 'doesntexist'
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'when not signed in' do
- it 'responds with status 404' do
- get action, id: 'doesntexist'
+ context 'when not signed in' do
+ it 'responds with status 404' do
+ get :raw, id: 'doesntexist'
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
end
end
diff --git a/spec/factories/notes.rb b/spec/factories/notes.rb
index 93f4903119c..44c3186d813 100644
--- a/spec/factories/notes.rb
+++ b/spec/factories/notes.rb
@@ -5,7 +5,7 @@ include ActionDispatch::TestProcess
FactoryGirl.define do
factory :note do
project factory: :empty_project
- note "Note"
+ note { generate(:title) }
author
on_issue
diff --git a/spec/factories/project_hooks.rb b/spec/factories/project_hooks.rb
index 39c2a9dd1fb..0210e871a63 100644
--- a/spec/factories/project_hooks.rb
+++ b/spec/factories/project_hooks.rb
@@ -1,6 +1,7 @@
FactoryGirl.define do
factory :project_hook do
url { generate(:url) }
+ enable_ssl_verification false
trait :token do
token { SecureRandom.hex(10) }
@@ -11,6 +12,7 @@ FactoryGirl.define do
merge_requests_events true
tag_push_events true
issues_events true
+ confidential_issues_events true
note_events true
build_events true
pipeline_events true
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index 0db2fe04edd..3580752a805 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -32,6 +32,10 @@ FactoryGirl.define do
request_access_enabled true
end
+ trait :with_avatar do
+ avatar { File.open(Rails.root.join('spec/fixtures/dk.png')) }
+ end
+
trait :repository do
# no-op... for now!
end
diff --git a/spec/features/admin/admin_hooks_spec.rb b/spec/features/admin/admin_hooks_spec.rb
index fb519a9bf12..c5f24d412d7 100644
--- a/spec/features/admin/admin_hooks_spec.rb
+++ b/spec/features/admin/admin_hooks_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe "Admin::Hooks", feature: true do
+describe 'Admin::Hooks', feature: true do
before do
@project = create(:project)
login_as :admin
@@ -8,24 +8,24 @@ describe "Admin::Hooks", feature: true do
@system_hook = create(:system_hook)
end
- describe "GET /admin/hooks" do
- it "is ok" do
+ describe 'GET /admin/hooks' do
+ it 'is ok' do
visit admin_root_path
- page.within ".layout-nav" do
- click_on "Hooks"
+ page.within '.layout-nav' do
+ click_on 'Hooks'
end
expect(current_path).to eq(admin_hooks_path)
end
- it "has hooks list" do
+ it 'has hooks list' do
visit admin_hooks_path
expect(page).to have_content(@system_hook.url)
end
end
- describe "New Hook" do
+ describe 'New Hook' do
let(:url) { generate(:url) }
it 'adds new hook' do
@@ -40,11 +40,36 @@ describe "Admin::Hooks", feature: true do
end
end
- describe "Test" do
+ describe 'Update existing hook' do
+ let(:new_url) { generate(:url) }
+
+ it 'updates existing hook' do
+ visit admin_hooks_path
+
+ click_link 'Edit'
+ fill_in 'hook_url', with: new_url
+ check 'Enable SSL verification'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'SSL Verification: enabled'
+ expect(current_path).to eq(admin_hooks_path)
+ expect(page).to have_content(new_url)
+ end
+ end
+
+ describe 'Remove existing hook' do
+ it 'remove existing hook' do
+ visit admin_hooks_path
+
+ expect { click_link 'Remove' }.to change(SystemHook, :count).by(-1)
+ end
+ end
+
+ describe 'Test' do
before do
WebMock.stub_request(:post, @system_hook.url)
visit admin_hooks_path
- click_link "Test hook"
+ click_link 'Test hook'
end
it { expect(current_path).to eq(admin_hooks_path) }
diff --git a/spec/features/admin/admin_requests_profiles_spec.rb b/spec/features/admin/admin_requests_profiles_spec.rb
new file mode 100644
index 00000000000..e8ecb70306b
--- /dev/null
+++ b/spec/features/admin/admin_requests_profiles_spec.rb
@@ -0,0 +1,69 @@
+require 'spec_helper'
+
+describe 'Admin::RequestsProfilesController', feature: true do
+ before do
+ FileUtils.mkdir_p(Gitlab::RequestProfiler::PROFILES_DIR)
+ login_as(:admin)
+ end
+
+ after do
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+
+ describe 'GET /admin/requests_profiles' do
+ it 'shows the current profile token' do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+
+ visit admin_requests_profiles_path
+
+ expect(page).to have_content("X-Profile-Token: #{Gitlab::RequestProfiler.profile_token}")
+ end
+
+ it 'lists all available profiles' do
+ time1 = 1.hour.ago
+ time2 = 2.hours.ago
+ time3 = 3.hours.ago
+ profile1 = "|gitlab-org|gitlab-ce_#{time1.to_i}.html"
+ profile2 = "|gitlab-org|gitlab-ce_#{time2.to_i}.html"
+ profile3 = "|gitlab-com|infrastructure_#{time3.to_i}.html"
+
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile1}")
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile2}")
+ FileUtils.touch("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile3}")
+
+ visit admin_requests_profiles_path
+
+ within('.panel', text: '/gitlab-org/gitlab-ce') do
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile1)}']", text: time1.to_s(:long))
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile2)}']", text: time2.to_s(:long))
+ end
+
+ within('.panel', text: '/gitlab-com/infrastructure') do
+ expect(page).to have_selector("a[href='#{admin_requests_profile_path(profile3)}']", text: time3.to_s(:long))
+ end
+ end
+ end
+
+ describe 'GET /admin/requests_profiles/:profile' do
+ context 'when a profile exists' do
+ it 'displays the content of the profile' do
+ content = 'This is a request profile'
+ profile = "|gitlab-org|gitlab-ce_#{Time.now.to_i}.html"
+
+ File.write("#{Gitlab::RequestProfiler::PROFILES_DIR}/#{profile}", content)
+
+ visit admin_requests_profile_path(profile)
+
+ expect(page).to have_content(content)
+ end
+ end
+
+ context 'when a profile does not exist' do
+ it 'shows an error message' do
+ visit admin_requests_profile_path('|non|existent_12345.html')
+
+ expect(page).to have_content('Profile not found')
+ end
+ end
+ end
+end
diff --git a/spec/features/explore/groups_list_spec.rb b/spec/features/explore/groups_list_spec.rb
index 8e5421a984b..9828cb179a7 100644
--- a/spec/features/explore/groups_list_spec.rb
+++ b/spec/features/explore/groups_list_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe 'Explore Groups page', js: true, feature: true do
+describe 'Explore Groups page', :js, :feature do
let!(:user) { create :user }
let!(:group) { create(:group) }
let!(:public_group) { create(:group, :public) }
@@ -46,19 +46,39 @@ describe 'Explore Groups page', js: true, feature: true do
it 'shows non-archived projects count' do
# Initially project is not archived
expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
-
+
# Archive project
empty_project.archive!
visit explore_groups_path
# Check project count
expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("0")
-
+
# Unarchive project
empty_project.unarchive!
visit explore_groups_path
# Check project count
- expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ expect(find('.js-groups-list-holder .content-list li:first-child .stats span:first-child')).to have_text("1")
+ end
+
+ describe 'landing component' do
+ it 'should show a landing component' do
+ expect(page).to have_content('Below you will find all the groups that are public.')
+ end
+
+ it 'should be dismissable' do
+ find('.dismiss-button').click
+
+ expect(page).not_to have_content('Below you will find all the groups that are public.')
+ end
+
+ it 'should persistently not show once dismissed' do
+ find('.dismiss-button').click
+
+ visit explore_groups_path
+
+ expect(page).not_to have_content('Below you will find all the groups that are public.')
+ end
end
end
diff --git a/spec/features/merge_requests/create_new_mr_spec.rb b/spec/features/merge_requests/create_new_mr_spec.rb
index e1353ec71ed..f0fec625108 100644
--- a/spec/features/merge_requests/create_new_mr_spec.rb
+++ b/spec/features/merge_requests/create_new_mr_spec.rb
@@ -35,6 +35,7 @@ feature 'Create New Merge Request', feature: true, js: true do
expect(page).to have_content('Target branch')
first('.js-target-branch').click
+ first('.dropdown-target-branch .dropdown-content')
first('.dropdown-target-branch .dropdown-content a', text: 'v1.1.0').click
expect(page).to have_content "b83d6e3"
diff --git a/spec/features/projects/blobs/blob_show_spec.rb b/spec/features/projects/blobs/blob_show_spec.rb
index 8613b850203..8dba2ccbafa 100644
--- a/spec/features/projects/blobs/blob_show_spec.rb
+++ b/spec/features/projects/blobs/blob_show_spec.rb
@@ -24,6 +24,9 @@ feature 'File blob', :js, feature: true do
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
end
@@ -50,6 +53,9 @@ feature 'File blob', :js, feature: true do
# shows a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
@@ -160,6 +166,9 @@ feature 'File blob', :js, feature: true do
# does not show a copy button
expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
@@ -203,6 +212,9 @@ feature 'File blob', :js, feature: true do
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
end
@@ -219,7 +231,7 @@ feature 'File blob', :js, feature: true do
branch_name: 'master',
commit_message: "Add PDF",
file_path: 'files/test.pdf',
- file_content: File.read(Rails.root.join('spec/javascripts/blob/pdf/test.pdf'))
+ file_content: project.repository.blob_at('add-pdf-file', 'files/pdf/test.pdf').data
).execute
visit_blob('files/test.pdf')
@@ -237,6 +249,9 @@ feature 'File blob', :js, feature: true do
# does not show a copy button
expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
@@ -262,6 +277,9 @@ feature 'File blob', :js, feature: true do
# does not show a copy button
expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
@@ -283,6 +301,9 @@ feature 'File blob', :js, feature: true do
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
end
end
end
@@ -305,6 +326,9 @@ feature 'File blob', :js, feature: true do
# does not show a copy button
expect(page).not_to have_selector('.js-copy-blob-source-btn')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
diff --git a/spec/features/projects/commit/cherry_pick_spec.rb b/spec/features/projects/commit/cherry_pick_spec.rb
index 5d64d42fd61..fa67d390c47 100644
--- a/spec/features/projects/commit/cherry_pick_spec.rb
+++ b/spec/features/projects/commit/cherry_pick_spec.rb
@@ -74,8 +74,10 @@ describe 'Cherry-pick Commits' do
wait_for_ajax
- page.within('#modal-cherry-pick-commit .dropdown-menu .dropdown-content') do
- click_link 'feature'
+ page.within('#modal-cherry-pick-commit .dropdown-menu') do
+ find('.dropdown-input input').set('feature')
+ wait_for_ajax
+ click_link "feature"
end
page.within('#modal-cherry-pick-commit') do
diff --git a/spec/features/projects/environments/environment_spec.rb b/spec/features/projects/environments/environment_spec.rb
index acc3efe04e6..1e12f8542e2 100644
--- a/spec/features/projects/environments/environment_spec.rb
+++ b/spec/features/projects/environments/environment_spec.rb
@@ -200,7 +200,7 @@ feature 'Environment', :feature do
end
scenario 'user deletes the branch with running environment' do
- visit namespace_project_branches_path(project.namespace, project)
+ visit namespace_project_branches_path(project.namespace, project, search: 'feature')
remove_branch_with_hooks(project, user, 'feature') do
page.within('.js-branch-feature') { find('a.btn-remove').click }
diff --git a/spec/features/projects/merge_request_button_spec.rb b/spec/features/projects/merge_request_button_spec.rb
index 05f3162f13c..1370ab1c521 100644
--- a/spec/features/projects/merge_request_button_spec.rb
+++ b/spec/features/projects/merge_request_button_spec.rb
@@ -85,8 +85,8 @@ feature 'Merge Request button', feature: true do
context 'on branches page' do
it_behaves_like 'Merge request button only shown when allowed' do
let(:label) { 'Merge request' }
- let(:url) { namespace_project_branches_path(project.namespace, project) }
- let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project) }
+ let(:url) { namespace_project_branches_path(project.namespace, project, search: 'feature') }
+ let(:fork_url) { namespace_project_branches_path(forked_project.namespace, forked_project, search: 'feature') }
end
end
diff --git a/spec/features/projects/settings/integration_settings_spec.rb b/spec/features/projects/settings/integration_settings_spec.rb
new file mode 100644
index 00000000000..7909234556e
--- /dev/null
+++ b/spec/features/projects/settings/integration_settings_spec.rb
@@ -0,0 +1,94 @@
+require 'spec_helper'
+
+feature 'Integration settings', feature: true do
+ let(:project) { create(:empty_project) }
+ let(:user) { create(:user) }
+ let(:role) { :developer }
+ let(:integrations_path) { namespace_project_settings_integrations_path(project.namespace, project) }
+
+ background do
+ login_as(user)
+ project.team << [user, role]
+ end
+
+ context 'for developer' do
+ given(:role) { :developer }
+
+ scenario 'to be disallowed to view' do
+ visit integrations_path
+
+ expect(page.status_code).to eq(404)
+ end
+ end
+
+ context 'for master' do
+ given(:role) { :master }
+
+ context 'Webhooks' do
+ let(:hook) { create(:project_hook, :all_events_enabled, enable_ssl_verification: true, project: project) }
+ let(:url) { generate(:url) }
+
+ scenario 'show list of webhooks' do
+ hook
+
+ visit integrations_path
+
+ expect(page.status_code).to eq(200)
+ expect(page).to have_content(hook.url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Push Events')
+ expect(page).to have_content('Tag Push Events')
+ expect(page).to have_content('Issues Events')
+ expect(page).to have_content('Confidential Issues Events')
+ expect(page).to have_content('Note Events')
+ expect(page).to have_content('Merge Requests Events')
+ expect(page).to have_content('Pipeline Events')
+ expect(page).to have_content('Wiki Page Events')
+ end
+
+ scenario 'create webhook' do
+ visit integrations_path
+
+ fill_in 'hook_url', with: url
+ check 'Tag push events'
+ check 'Enable SSL verification'
+
+ click_button 'Add webhook'
+
+ expect(page).to have_content(url)
+ expect(page).to have_content('SSL Verification: enabled')
+ expect(page).to have_content('Push Events')
+ expect(page).to have_content('Tag Push Events')
+ end
+
+ scenario 'edit existing webhook' do
+ hook
+ visit integrations_path
+
+ click_link 'Edit'
+ fill_in 'hook_url', with: url
+ check 'Enable SSL verification'
+ click_button 'Save changes'
+
+ expect(page).to have_content 'SSL Verification: enabled'
+ expect(page).to have_content(url)
+ end
+
+ scenario 'test existing webhook' do
+ WebMock.stub_request(:post, hook.url)
+ visit integrations_path
+
+ click_link 'Test'
+
+ expect(current_path).to eq(integrations_path)
+ end
+
+ scenario 'remove existing webhook' do
+ hook
+ visit integrations_path
+
+ expect { click_link 'Remove' }.to change(ProjectHook, :count).by(-1)
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/snippets/show_spec.rb b/spec/features/projects/snippets/show_spec.rb
index 7eb1210e307..cedf3778c7e 100644
--- a/spec/features/projects/snippets/show_spec.rb
+++ b/spec/features/projects/snippets/show_spec.rb
@@ -30,6 +30,12 @@ feature 'Project snippet', :js, feature: true do
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
@@ -59,6 +65,12 @@ feature 'Project snippet', :js, feature: true do
# shows a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
diff --git a/spec/features/security/project/internal_access_spec.rb b/spec/features/security/project/internal_access_spec.rb
index a1a36931824..26879a77c48 100644
--- a/spec/features/security/project/internal_access_spec.rb
+++ b/spec/features/security/project/internal_access_spec.rb
@@ -466,6 +466,21 @@ describe "Internal Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/environments/:id/deployments" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_deployments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
diff --git a/spec/features/security/project/private_access_spec.rb b/spec/features/security/project/private_access_spec.rb
index 5d58494a22a..699ca4f724c 100644
--- a/spec/features/security/project/private_access_spec.rb
+++ b/spec/features/security/project/private_access_spec.rb
@@ -449,6 +449,21 @@ describe "Private Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/environments/:id/deployments" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_deployments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
diff --git a/spec/features/security/project/public_access_spec.rb b/spec/features/security/project/public_access_spec.rb
index 5df5b710dc4..624f0d0f485 100644
--- a/spec/features/security/project/public_access_spec.rb
+++ b/spec/features/security/project/public_access_spec.rb
@@ -286,6 +286,21 @@ describe "Public Project Access", feature: true do
it { is_expected.to be_denied_for(:visitor) }
end
+ describe "GET /:project_path/environments/:id/deployments" do
+ let(:environment) { create(:environment, project: project) }
+ subject { namespace_project_environment_deployments_path(project.namespace, project, environment) }
+
+ it { is_expected.to be_allowed_for(:admin) }
+ it { is_expected.to be_allowed_for(:owner).of(project) }
+ it { is_expected.to be_allowed_for(:master).of(project) }
+ it { is_expected.to be_allowed_for(:developer).of(project) }
+ it { is_expected.to be_allowed_for(:reporter).of(project) }
+ it { is_expected.to be_denied_for(:guest).of(project) }
+ it { is_expected.to be_denied_for(:user) }
+ it { is_expected.to be_denied_for(:external) }
+ it { is_expected.to be_denied_for(:visitor) }
+ end
+
describe "GET /:project_path/environments/new" do
subject { new_namespace_project_environment_path(project.namespace, project) }
diff --git a/spec/features/snippets/notes_on_personal_snippets_spec.rb b/spec/features/snippets/notes_on_personal_snippets_spec.rb
new file mode 100644
index 00000000000..c646039e0b1
--- /dev/null
+++ b/spec/features/snippets/notes_on_personal_snippets_spec.rb
@@ -0,0 +1,39 @@
+require 'spec_helper'
+
+describe 'Comments on personal snippets', feature: true do
+ let!(:user) { create(:user) }
+ let!(:snippet) { create(:personal_snippet, :public) }
+ let!(:snippet_notes) do
+ [
+ create(:note_on_personal_snippet, noteable: snippet, author: user),
+ create(:note_on_personal_snippet, noteable: snippet)
+ ]
+ end
+ let!(:other_note) { create(:note_on_personal_snippet) }
+
+ before do
+ login_as user
+ visit snippet_path(snippet)
+ end
+
+ subject { page }
+
+ context 'viewing the snippet detail page' do
+ it 'contains notes for a snippet with correct action icons' do
+ expect(page).to have_selector('#notes-list li', count: 2)
+
+ # comment authored by current user
+ page.within("#notes-list li#note_#{snippet_notes[0].id}") do
+ expect(page).to have_content(snippet_notes[0].note)
+ expect(page).to have_selector('.js-note-delete')
+ expect(page).to have_selector('.note-emoji-button')
+ end
+
+ page.within("#notes-list li#note_#{snippet_notes[1].id}") do
+ expect(page).to have_content(snippet_notes[1].note)
+ expect(page).not_to have_selector('.js-note-delete')
+ expect(page).to have_selector('.note-emoji-button')
+ end
+ end
+ end
+end
diff --git a/spec/features/snippets/show_spec.rb b/spec/features/snippets/show_spec.rb
index cebcba6a230..e36cf547f80 100644
--- a/spec/features/snippets/show_spec.rb
+++ b/spec/features/snippets/show_spec.rb
@@ -24,6 +24,12 @@ feature 'Snippet', :js, feature: true do
# shows an enabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn:not(.disabled)')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
end
@@ -53,6 +59,12 @@ feature 'Snippet', :js, feature: true do
# shows a disabled copy button
expect(page).to have_selector('.js-copy-blob-source-btn.disabled')
+
+ # shows a raw button
+ expect(page).to have_link('Open raw')
+
+ # shows a download button
+ expect(page).to have_link('Download')
end
end
diff --git a/spec/finders/notes_finder_spec.rb b/spec/finders/notes_finder_spec.rb
index 765bf44d863..ba6bbb3bce0 100644
--- a/spec/finders/notes_finder_spec.rb
+++ b/spec/finders/notes_finder_spec.rb
@@ -110,6 +110,15 @@ describe NotesFinder do
expect(notes.count).to eq(1)
end
+ it 'finds notes on personal snippets' do
+ note = create(:note_on_personal_snippet)
+ params = { target_type: 'personal_snippet', target_id: note.noteable_id }
+
+ notes = described_class.new(project, user, params).execute
+
+ expect(notes.count).to eq(1)
+ end
+
it 'raises an exception for an invalid target_type' do
params[:target_type] = 'invalid'
expect { described_class.new(project, user, params).execute }.to raise_error('invalid target_type')
diff --git a/spec/fixtures/api/schemas/deployments.json b/spec/fixtures/api/schemas/deployments.json
new file mode 100644
index 00000000000..1112f23aab2
--- /dev/null
+++ b/spec/fixtures/api/schemas/deployments.json
@@ -0,0 +1,58 @@
+{
+ "additionalProperties": false,
+ "properties": {
+ "deployments": {
+ "items": {
+ "additionalProperties": false,
+ "properties": {
+ "created_at": {
+ "type": "string"
+ },
+ "id": {
+ "type": "integer"
+ },
+ "iid": {
+ "type": "integer"
+ },
+ "last?": {
+ "type": "boolean"
+ },
+ "ref": {
+ "additionalProperties": false,
+ "properties": {
+ "name": {
+ "type": "string"
+ }
+ },
+ "required": [
+ "name"
+ ],
+ "type": "object"
+ },
+ "sha": {
+ "type": "string"
+ },
+ "tag": {
+ "type": "boolean"
+ }
+ },
+ "required": [
+ "sha",
+ "created_at",
+ "iid",
+ "tag",
+ "last?",
+ "ref",
+ "id"
+ ],
+ "type": "object"
+ },
+ "minItems": 1,
+ "type": "array"
+ }
+ },
+ "required": [
+ "deployments"
+ ],
+ "type": "object"
+}
diff --git a/spec/helpers/award_emoji_helper_spec.rb b/spec/helpers/award_emoji_helper_spec.rb
new file mode 100644
index 00000000000..7dfd6a3f6b4
--- /dev/null
+++ b/spec/helpers/award_emoji_helper_spec.rb
@@ -0,0 +1,61 @@
+require 'spec_helper'
+
+describe AwardEmojiHelper do
+ describe '.toggle_award_url' do
+ context 'note on personal snippet' do
+ let(:note) { create(:note_on_personal_snippet) }
+
+ it 'returns correct url' do
+ expected_url = "/snippets/#{note.noteable.id}/notes/#{note.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(note)).to eq(expected_url)
+ end
+ end
+
+ context 'note on project item' do
+ let(:note) { create(:note_on_project_snippet) }
+
+ it 'returns correct url' do
+ @project = note.noteable.project
+
+ expected_url = "/#{@project.namespace.path}/#{@project.path}/notes/#{note.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(note)).to eq(expected_url)
+ end
+ end
+
+ context 'personal snippet' do
+ let(:snippet) { create(:personal_snippet) }
+
+ it 'returns correct url' do
+ expected_url = "/snippets/#{snippet.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(snippet)).to eq(expected_url)
+ end
+ end
+
+ context 'merge request' do
+ let(:merge_request) { create(:merge_request) }
+
+ it 'returns correct url' do
+ @project = merge_request.project
+
+ expected_url = "/#{@project.namespace.path}/#{@project.path}/merge_requests/#{merge_request.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(merge_request)).to eq(expected_url)
+ end
+ end
+
+ context 'issue' do
+ let(:issue) { create(:issue) }
+
+ it 'returns correct url' do
+ @project = issue.project
+
+ expected_url = "/#{@project.namespace.path}/#{@project.path}/issues/#{issue.id}/toggle_award_emoji"
+
+ expect(helper.toggle_award_url(issue)).to eq(expected_url)
+ end
+ end
+ end
+end
diff --git a/spec/helpers/merge_requests_helper_spec.rb b/spec/helpers/merge_requests_helper_spec.rb
index e9037749ef2..10681af5f7e 100644
--- a/spec/helpers/merge_requests_helper_spec.rb
+++ b/spec/helpers/merge_requests_helper_spec.rb
@@ -64,7 +64,7 @@ describe MergeRequestsHelper do
it do
@project = project
-
+
is_expected.to eq("#1, #2, and #{other_project.namespace.path}/#{other_project.path}#3")
end
end
@@ -149,6 +149,50 @@ describe MergeRequestsHelper do
end
end
+ describe '#target_projects' do
+ let(:project) { create(:empty_project) }
+ let(:fork_project) { create(:empty_project, forked_from_project: project) }
+
+ context 'when target project has enabled merge requests' do
+ it 'returns the forked_from project' do
+ expect(target_projects(fork_project)).to contain_exactly(project, fork_project)
+ end
+ end
+
+ context 'when target project has disabled merge requests' do
+ it 'returns the forked project' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ expect(target_projects(fork_project)).to contain_exactly(fork_project)
+ end
+ end
+ end
+
+ describe '#new_mr_path_from_push_event' do
+ subject(:url_params) { URI.decode_www_form(new_mr_path_from_push_event(event)).to_h }
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project, creator: user) }
+ let(:fork_project) { create(:project, forked_from_project: project, creator: user) }
+ let(:event) do
+ push_data = Gitlab::DataBuilder::Push.build_sample(fork_project, user)
+ create(:event, :pushed, project: fork_project, target: fork_project, author: user, data: push_data)
+ end
+
+ context 'when target project has enabled merge requests' do
+ it 'returns link to create merge request on source project' do
+ expect(url_params['merge_request[target_project_id]'].to_i).to eq(project.id)
+ end
+ end
+
+ context 'when target project has disabled merge requests' do
+ it 'returns link to create merge request on forked project' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ expect(url_params['merge_request[target_project_id]'].to_i).to eq(fork_project.id)
+ end
+ end
+ end
+
describe '#mr_issues_mentioned_but_not_closing' do
let(:user_1) { create(:user) }
let(:user_2) { create(:user) }
diff --git a/spec/javascripts/blob/pdf/index_spec.js b/spec/javascripts/blob/pdf/index_spec.js
index d3a4d04345b..bbeaf95e68d 100644
--- a/spec/javascripts/blob/pdf/index_spec.js
+++ b/spec/javascripts/blob/pdf/index_spec.js
@@ -1,5 +1,7 @@
+/* eslint-disable import/no-unresolved */
+
import renderPDF from '~/blob/pdf';
-import testPDF from './test.pdf';
+import testPDF from '../../fixtures/blob/pdf/test.pdf';
describe('PDF renderer', () => {
let viewer;
@@ -59,7 +61,7 @@ describe('PDF renderer', () => {
describe('error getting file', () => {
beforeEach((done) => {
- viewer.dataset.endpoint = 'invalid/endpoint';
+ viewer.dataset.endpoint = 'invalid/path/to/file.pdf';
app = renderPDF();
checkLoaded(done);
diff --git a/spec/javascripts/blob/pdf/test.pdf b/spec/javascripts/blob/pdf/test.pdf
deleted file mode 100644
index eb3d147fde3..00000000000
--- a/spec/javascripts/blob/pdf/test.pdf
+++ /dev/null
Binary files differ
diff --git a/spec/javascripts/fixtures/environments.rb b/spec/javascripts/fixtures/environments.rb
new file mode 100644
index 00000000000..3474f4696ef
--- /dev/null
+++ b/spec/javascripts/fixtures/environments.rb
@@ -0,0 +1,30 @@
+require 'spec_helper'
+
+describe Projects::EnvironmentsController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'environments-project') }
+ let(:environment) { create(:environment, name: 'production', project: project) }
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('environments/metrics')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'environments/metrics/metrics.html.raw' do |example|
+ get :metrics,
+ namespace_id: project.namespace,
+ project_id: project,
+ id: environment.id
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/fixtures/environments/metrics.html.haml b/spec/javascripts/fixtures/environments/metrics.html.haml
deleted file mode 100644
index e2dd9519898..00000000000
--- a/spec/javascripts/fixtures/environments/metrics.html.haml
+++ /dev/null
@@ -1,62 +0,0 @@
-.prometheus-container{ 'data-has-metrics': "false", 'data-doc-link': '/help/administration/monitoring/prometheus/index.md', 'data-prometheus-integration': '/root/hello-prometheus/services/prometheus/edit' }
- .top-area
- .row
- .col-sm-6
- %h3.page-title
- Metrics for environment
- .prometheus-state
- .js-getting-started.hidden
- .row
- .col-md-4.col-md-offset-4.state-svg
- %svg
- .row
- .col-md-6.col-md-offset-3
- %h4.text-center.state-title
- Get started with performance monitoring
- .row
- .col-md-6.col-md-offset-3
- .description-text.text-center.state-description
- Stay updated about the performance and health of your environment by configuring Prometheus to monitor your deployments. Learn more about performance monitoring
- .row.state-button-section
- .col-md-4.col-md-offset-4.text-center.state-button
- %a.btn.btn-success
- Configure Prometheus
- .js-loading.hidden
- .row
- .col-md-4.col-md-offset-4.state-svg
- %svg
- .row
- .col-md-6.col-md-offset-3
- %h4.text-center.state-title
- Waiting for performance data
- .row
- .col-md-6.col-md-offset-3
- .description-text.text-center.state-description
- Creating graphs uses the data from the Prometheus server. If this takes a long time, ensure that data is available.
- .row.state-button-section
- .col-md-4.col-md-offset-4.text-center.state-button
- %a.btn.btn-success
- View documentation
- .js-unable-to-connect.hidden
- .row
- .col-md-4.col-md-offset-4.state-svg
- %svg
- .row
- .col-md-6.col-md-offset-3
- %h4.text-center.state-title
- Unable to connect to Prometheus server
- .row
- .col-md-6.col-md-offset-3
- .description-text.text-center.state-description
- Ensure connectivity is available from the GitLab server to the Prometheus server
- .row.state-button-section
- .col-md-4.col-md-offset-4.text-center.state-button
- %a.btn.btn-success
- View documentation
- .prometheus-graphs
- .row
- .col-sm-12
- %svg.prometheus-graph{ 'graph-type' => 'cpu_values' }
- .row
- .col-sm-12
- %svg.prometheus-graph{ 'graph-type' => 'memory_values' }
diff --git a/spec/javascripts/fixtures/pdf.rb b/spec/javascripts/fixtures/pdf.rb
new file mode 100644
index 00000000000..6b2422a7986
--- /dev/null
+++ b/spec/javascripts/fixtures/pdf.rb
@@ -0,0 +1,18 @@
+require 'spec_helper'
+
+describe 'PDF file', '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project, namespace: namespace, path: 'pdf-project') }
+
+ before(:all) do
+ clean_frontend_fixtures('blob/pdf/')
+ end
+
+ it 'blob/pdf/test.pdf' do |example|
+ blob = project.repository.blob_at('e774ebd33', 'files/pdf/test.pdf')
+
+ store_frontend_fixture(blob.data.force_encoding("utf-8"), example.description)
+ end
+end
diff --git a/spec/javascripts/landing_spec.js b/spec/javascripts/landing_spec.js
new file mode 100644
index 00000000000..7916073190a
--- /dev/null
+++ b/spec/javascripts/landing_spec.js
@@ -0,0 +1,160 @@
+import Landing from '~/landing';
+import Cookies from 'js-cookie';
+
+describe('Landing', function () {
+ describe('class constructor', function () {
+ beforeEach(function () {
+ this.landingElement = {};
+ this.dismissButton = {};
+ this.cookieName = 'cookie_name';
+
+ this.landing = new Landing(this.landingElement, this.dismissButton, this.cookieName);
+ });
+
+ it('should set .landing', function () {
+ expect(this.landing.landingElement).toBe(this.landingElement);
+ });
+
+ it('should set .cookieName', function () {
+ expect(this.landing.cookieName).toBe(this.cookieName);
+ });
+
+ it('should set .dismissButton', function () {
+ expect(this.landing.dismissButton).toBe(this.dismissButton);
+ });
+
+ it('should set .eventWrapper', function () {
+ expect(this.landing.eventWrapper).toEqual({});
+ });
+ });
+
+ describe('toggle', function () {
+ beforeEach(function () {
+ this.isDismissed = false;
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
+ this.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: this.landingElement,
+ };
+
+ spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
+ spyOn(this.landing, 'addEvents');
+
+ Landing.prototype.toggle.call(this.landing);
+ });
+
+ it('should call .isDismissed', function () {
+ expect(this.landing.isDismissed).toHaveBeenCalled();
+ });
+
+ it('should call .classList.toggle', function () {
+ expect(this.landingElement.classList.toggle).toHaveBeenCalledWith('hidden', this.isDismissed);
+ });
+
+ it('should call .addEvents', function () {
+ expect(this.landing.addEvents).toHaveBeenCalled();
+ });
+
+ describe('if isDismissed is true', function () {
+ beforeEach(function () {
+ this.isDismissed = true;
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['toggle']) };
+ this.landing = {
+ isDismissed: () => {},
+ addEvents: () => {},
+ landingElement: this.landingElement,
+ };
+
+ spyOn(this.landing, 'isDismissed').and.returnValue(this.isDismissed);
+ spyOn(this.landing, 'addEvents');
+
+ this.landing.isDismissed.calls.reset();
+
+ Landing.prototype.toggle.call(this.landing);
+ });
+
+ it('should not call .addEvents', function () {
+ expect(this.landing.addEvents).not.toHaveBeenCalled();
+ });
+ });
+ });
+
+ describe('addEvents', function () {
+ beforeEach(function () {
+ this.dismissButton = jasmine.createSpyObj('dismissButton', ['addEventListener']);
+ this.eventWrapper = {};
+ this.landing = {
+ eventWrapper: this.eventWrapper,
+ dismissButton: this.dismissButton,
+ dismissLanding: () => {},
+ };
+
+ Landing.prototype.addEvents.call(this.landing);
+ });
+
+ it('should set .eventWrapper.dismissLanding', function () {
+ expect(this.eventWrapper.dismissLanding).toEqual(jasmine.any(Function));
+ });
+
+ it('should call .addEventListener', function () {
+ expect(this.dismissButton.addEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding);
+ });
+ });
+
+ describe('removeEvents', function () {
+ beforeEach(function () {
+ this.dismissButton = jasmine.createSpyObj('dismissButton', ['removeEventListener']);
+ this.eventWrapper = { dismissLanding: () => {} };
+ this.landing = {
+ eventWrapper: this.eventWrapper,
+ dismissButton: this.dismissButton,
+ };
+
+ Landing.prototype.removeEvents.call(this.landing);
+ });
+
+ it('should call .removeEventListener', function () {
+ expect(this.dismissButton.removeEventListener).toHaveBeenCalledWith('click', this.eventWrapper.dismissLanding);
+ });
+ });
+
+ describe('dismissLanding', function () {
+ beforeEach(function () {
+ this.landingElement = { classList: jasmine.createSpyObj('classList', ['add']) };
+ this.cookieName = 'cookie_name';
+ this.landing = { landingElement: this.landingElement, cookieName: this.cookieName };
+
+ spyOn(Cookies, 'set');
+
+ Landing.prototype.dismissLanding.call(this.landing);
+ });
+
+ it('should call .classList.add', function () {
+ expect(this.landingElement.classList.add).toHaveBeenCalledWith('hidden');
+ });
+
+ it('should call Cookies.set', function () {
+ expect(Cookies.set).toHaveBeenCalledWith(this.cookieName, 'true', { expires: 365 });
+ });
+ });
+
+ describe('isDismissed', function () {
+ beforeEach(function () {
+ this.cookieName = 'cookie_name';
+ this.landing = { cookieName: this.cookieName };
+
+ spyOn(Cookies, 'get').and.returnValue('true');
+
+ this.isDismissed = Landing.prototype.isDismissed.call(this.landing);
+ });
+
+ it('should call Cookies.get', function () {
+ expect(Cookies.get).toHaveBeenCalledWith(this.cookieName);
+ });
+
+ it('should return a boolean', function () {
+ expect(typeof this.isDismissed).toEqual('boolean');
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/deployments_spec.js b/spec/javascripts/monitoring/deployments_spec.js
new file mode 100644
index 00000000000..19bc11d0f24
--- /dev/null
+++ b/spec/javascripts/monitoring/deployments_spec.js
@@ -0,0 +1,133 @@
+import d3 from 'd3';
+import PrometheusGraph from '~/monitoring/prometheus_graph';
+import Deployments from '~/monitoring/deployments';
+import { prometheusMockData } from './prometheus_mock_data';
+
+describe('Metrics deployments', () => {
+ const fixtureName = 'environments/metrics/metrics.html.raw';
+ let deployment;
+ let prometheusGraph;
+
+ const graphElement = () => document.querySelector('.prometheus-graph');
+
+ preloadFixtures(fixtureName);
+
+ beforeEach((done) => {
+ // Setup the view
+ loadFixtures(fixtureName);
+
+ d3.selectAll('.prometheus-graph')
+ .append('g')
+ .attr('class', 'graph-container');
+
+ prometheusGraph = new PrometheusGraph();
+ deployment = new Deployments(1000, 500);
+
+ spyOn(prometheusGraph, 'init');
+ spyOn($, 'ajax').and.callFake(() => {
+ const d = $.Deferred();
+ d.resolve({
+ deployments: [{
+ id: 1,
+ created_at: deployment.chartData[10].time,
+ sha: 'testing',
+ tag: false,
+ ref: {
+ name: 'testing',
+ },
+ }, {
+ id: 2,
+ created_at: deployment.chartData[15].time,
+ sha: '',
+ tag: true,
+ ref: {
+ name: 'tag',
+ },
+ }],
+ });
+
+ setTimeout(done);
+
+ return d.promise();
+ });
+
+ prometheusGraph.configureGraph();
+ prometheusGraph.transformData(prometheusMockData.metrics);
+
+ deployment.init(prometheusGraph.graphSpecificProperties.memory_values.data);
+ });
+
+ it('creates line on graph for deploment', () => {
+ expect(
+ graphElement().querySelectorAll('.deployment-line').length,
+ ).toBe(2);
+ });
+
+ it('creates hidden deploy boxes', () => {
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box').length,
+ ).toBe(2);
+ });
+
+ it('hides the info boxes by default', () => {
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
+ ).toBe(2);
+ });
+
+ it('shows sha short code when tag is false', () => {
+ expect(
+ graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box').textContent.trim(),
+ ).toContain('testin');
+ });
+
+ it('shows ref name when tag is true', () => {
+ expect(
+ graphElement().querySelector('.deploy-info-2-cpu_values .js-deploy-info-box').textContent.trim(),
+ ).toContain('tag');
+ });
+
+ it('shows info box when moving mouse over line', () => {
+ deployment.mouseOverDeployInfo(deployment.data[0].xPos, 'cpu_values');
+
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
+ ).toBe(1);
+
+ expect(
+ graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'),
+ ).toBeNull();
+ });
+
+ it('hides previously visible info box when moving mouse away', () => {
+ deployment.mouseOverDeployInfo(500, 'cpu_values');
+
+ expect(
+ graphElement().querySelectorAll('.prometheus-graph .js-deploy-info-box.hidden').length,
+ ).toBe(2);
+
+ expect(
+ graphElement().querySelector('.deploy-info-1-cpu_values .js-deploy-info-box.hidden'),
+ ).not.toBeNull();
+ });
+
+ describe('refText', () => {
+ it('returns shortened SHA', () => {
+ expect(
+ Deployments.refText({
+ tag: false,
+ sha: '123456789',
+ }),
+ ).toBe('123456');
+ });
+
+ it('returns tag name', () => {
+ expect(
+ Deployments.refText({
+ tag: true,
+ ref: 'v1.0',
+ }),
+ ).toBe('v1.0');
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/prometheus_graph_spec.js b/spec/javascripts/monitoring/prometheus_graph_spec.js
index 4b904fc2960..25578bf1c6e 100644
--- a/spec/javascripts/monitoring/prometheus_graph_spec.js
+++ b/spec/javascripts/monitoring/prometheus_graph_spec.js
@@ -3,7 +3,7 @@ import PrometheusGraph from '~/monitoring/prometheus_graph';
import { prometheusMockData } from './prometheus_mock_data';
describe('PrometheusGraph', () => {
- const fixtureName = 'static/environments/metrics.html.raw';
+ const fixtureName = 'environments/metrics/metrics.html.raw';
const prometheusGraphContainer = '.prometheus-graph';
const prometheusGraphContents = `${prometheusGraphContainer}[graph-type=cpu_values]`;
@@ -77,7 +77,7 @@ describe('PrometheusGraph', () => {
});
describe('PrometheusGraphs UX states', () => {
- const fixtureName = 'static/environments/metrics.html.raw';
+ const fixtureName = 'environments/metrics/metrics.html.raw';
preloadFixtures(fixtureName);
beforeEach(() => {
diff --git a/spec/javascripts/pdf/index_spec.js b/spec/javascripts/pdf/index_spec.js
new file mode 100644
index 00000000000..f661fae5fe2
--- /dev/null
+++ b/spec/javascripts/pdf/index_spec.js
@@ -0,0 +1,61 @@
+/* eslint-disable import/no-unresolved */
+
+import Vue from 'vue';
+import { PDFJS } from 'pdfjs-dist';
+import workerSrc from 'vendor/pdf.worker';
+
+import PDFLab from '~/pdf/index.vue';
+import pdf from '../fixtures/blob/pdf/test.pdf';
+
+PDFJS.workerSrc = workerSrc;
+const Component = Vue.extend(PDFLab);
+
+describe('PDF component', () => {
+ let vm;
+
+ const checkLoaded = (done) => {
+ if (vm.loading) {
+ setTimeout(() => {
+ checkLoaded(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
+ describe('without PDF data', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ pdf: '',
+ },
+ });
+
+ vm.$mount();
+
+ checkLoaded(done);
+ });
+
+ it('does not render', () => {
+ expect(vm.$el.tagName).toBeUndefined();
+ });
+ });
+
+ describe('with PDF data', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ pdf,
+ },
+ });
+
+ vm.$mount();
+
+ checkLoaded(done);
+ });
+
+ it('renders pdf component', () => {
+ expect(vm.$el.tagName).toBeDefined();
+ });
+ });
+});
diff --git a/spec/javascripts/pdf/page_spec.js b/spec/javascripts/pdf/page_spec.js
new file mode 100644
index 00000000000..ac76ebbfbe6
--- /dev/null
+++ b/spec/javascripts/pdf/page_spec.js
@@ -0,0 +1,57 @@
+/* eslint-disable import/no-unresolved */
+
+import Vue from 'vue';
+import pdfjsLib from 'pdfjs-dist';
+import workerSrc from 'vendor/pdf.worker';
+
+import PageComponent from '~/pdf/page/index.vue';
+import testPDF from '../fixtures/blob/pdf/test.pdf';
+
+const Component = Vue.extend(PageComponent);
+
+describe('Page component', () => {
+ let vm;
+ let testPage;
+ pdfjsLib.PDFJS.workerSrc = workerSrc;
+
+ const checkRendered = (done) => {
+ if (vm.rendering) {
+ setTimeout(() => {
+ checkRendered(done);
+ }, 100);
+ } else {
+ done();
+ }
+ };
+
+ beforeEach((done) => {
+ pdfjsLib.getDocument(testPDF)
+ .then(pdf => pdf.getPage(1))
+ .then((page) => {
+ testPage = page;
+ done();
+ })
+ .catch((error) => {
+ console.error(error);
+ });
+ });
+
+ describe('render', () => {
+ beforeEach((done) => {
+ vm = new Component({
+ propsData: {
+ page: testPage,
+ number: 1,
+ },
+ });
+
+ vm.$mount();
+
+ checkRendered(done);
+ });
+
+ it('renders first page', () => {
+ expect(vm.$el.tagName).toBeDefined();
+ });
+ });
+});
diff --git a/spec/lib/constraints/group_url_constrainer_spec.rb b/spec/lib/constraints/group_url_constrainer_spec.rb
index 96dacdc5cd2..f95adf3a84b 100644
--- a/spec/lib/constraints/group_url_constrainer_spec.rb
+++ b/spec/lib/constraints/group_url_constrainer_spec.rb
@@ -17,6 +17,13 @@ describe GroupUrlConstrainer, lib: true do
it { expect(subject.matches?(request)).to be_truthy }
end
+ context 'valid request for nested group with reserved top level name' do
+ let!(:nested_group) { create(:group, path: 'api', parent: group) }
+ let!(:request) { build_request('gitlab/api') }
+
+ it { expect(subject.matches?(request)).to be_truthy }
+ end
+
context 'invalid request' do
let(:request) { build_request('foo') }
diff --git a/spec/lib/gitlab/database/migration_helpers_spec.rb b/spec/lib/gitlab/database/migration_helpers_spec.rb
index a044b871730..737fac14f92 100644
--- a/spec/lib/gitlab/database/migration_helpers_spec.rb
+++ b/spec/lib/gitlab/database/migration_helpers_spec.rb
@@ -726,4 +726,37 @@ describe Gitlab::Database::MigrationHelpers, lib: true do
expect(model.column_for(:users, :kittens)).to be_nil
end
end
+
+ describe '#replace_sql' do
+ context 'using postgres' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(false)
+ end
+
+ it 'builds the sql with correct functions' do
+ expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s).
+ to include('regexp_replace')
+ end
+ end
+
+ context 'using mysql' do
+ before do
+ allow(Gitlab::Database).to receive(:mysql?).and_return(true)
+ end
+
+ it 'builds the sql with the correct functions' do
+ expect(model.replace_sql(Arel::Table.new(:users)[:first_name], "Alice", "Eve").to_s).
+ to include('locate', 'insert')
+ end
+ end
+
+ describe 'results' do
+ let!(:user) { create(:user, name: 'Kathy Alice Aliceson') }
+
+ it 'replaces the correct part of the string' do
+ model.update_column_in_batches(:users, :name, model.replace_sql(Arel::Table.new(:users)[:name], 'Alice', 'Eve'))
+ expect(user.reload.name).to eq('Kathy Eve Aliceson')
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
new file mode 100644
index 00000000000..64bc5fc0429
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_base_spec.rb
@@ -0,0 +1,197 @@
+require 'spec_helper'
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameBase do
+ let(:migration) { FakeRenameReservedPathMigrationV1.new }
+ let(:subject) { described_class.new(['the-path'], migration) }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ def migration_namespace(namespace)
+ Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::
+ Namespace.find(namespace.id)
+ end
+
+ def migration_project(project)
+ Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::
+ Project.find(project.id)
+ end
+
+ describe "#remove_last_ocurrence" do
+ it "removes only the last occurance of a string" do
+ input = "this/is/a-word-to-replace/namespace/with/a-word-to-replace"
+
+ expect(subject.remove_last_occurrence(input, "a-word-to-replace"))
+ .to eq("this/is/a-word-to-replace/namespace/with/")
+ end
+ end
+
+ describe '#remove_cached_html_for_projects' do
+ let(:project) { create(:empty_project, description_html: 'Project description') }
+
+ it 'removes description_html from projects' do
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(project.reload.description_html).to be_nil
+ end
+
+ it 'removes issue descriptions' do
+ issue = create(:issue, project: project, description_html: 'Issue description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(issue.reload.description_html).to be_nil
+ end
+
+ it 'removes merge request descriptions' do
+ merge_request = create(:merge_request,
+ source_project: project,
+ target_project: project,
+ description_html: 'MergeRequest description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(merge_request.reload.description_html).to be_nil
+ end
+
+ it 'removes note html' do
+ note = create(:note,
+ project: project,
+ noteable: create(:issue, project: project),
+ note_html: 'note description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(note.reload.note_html).to be_nil
+ end
+
+ it 'removes milestone description' do
+ milestone = create(:milestone,
+ project: project,
+ description_html: 'milestone description')
+
+ subject.remove_cached_html_for_projects([project.id])
+
+ expect(milestone.reload.description_html).to be_nil
+ end
+ end
+
+ describe '#rename_path_for_routable' do
+ context 'for namespaces' do
+ let(:namespace) { create(:namespace, path: 'the-path') }
+ it "renames namespaces called the-path" do
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(namespace.reload.path).to eq("the-path0")
+ end
+
+ it "renames the route to the namespace" do
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(Namespace.find(namespace.id).full_path).to eq("the-path0")
+ end
+
+ it "renames the route for projects of the namespace" do
+ project = create(:project, path: "project-path", namespace: namespace)
+
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(project.route.reload.path).to eq("the-path0/project-path")
+ end
+
+ it 'returns the old & the new path' do
+ old_path, new_path = subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(old_path).to eq('the-path')
+ expect(new_path).to eq('the-path0')
+ end
+
+ context "the-path namespace -> subgroup -> the-path0 project" do
+ it "updates the route of the project correctly" do
+ subgroup = create(:group, path: "subgroup", parent: namespace)
+ project = create(:project, path: "the-path0", namespace: subgroup)
+
+ subject.rename_path_for_routable(migration_namespace(namespace))
+
+ expect(project.route.reload.path).to eq("the-path0/subgroup/the-path0")
+ end
+ end
+ end
+
+ context 'for projects' do
+ let(:parent) { create(:namespace, path: 'the-parent') }
+ let(:project) { create(:empty_project, path: 'the-path', namespace: parent) }
+
+ it 'renames the project called `the-path`' do
+ subject.rename_path_for_routable(migration_project(project))
+
+ expect(project.reload.path).to eq('the-path0')
+ end
+
+ it 'renames the route for the project' do
+ subject.rename_path_for_routable(project)
+
+ expect(project.reload.route.path).to eq('the-parent/the-path0')
+ end
+
+ it 'returns the old & new path' do
+ old_path, new_path = subject.rename_path_for_routable(migration_project(project))
+
+ expect(old_path).to eq('the-parent/the-path')
+ expect(new_path).to eq('the-parent/the-path0')
+ end
+ end
+ end
+
+ describe '#move_pages' do
+ it 'moves the pages directory' do
+ expect(subject).to receive(:move_folders)
+ .with(TestEnv.pages_path, 'old-path', 'new-path')
+
+ subject.move_pages('old-path', 'new-path')
+ end
+ end
+
+ describe "#move_uploads" do
+ let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
+ let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
+
+ it 'moves subdirectories in the uploads folder' do
+ expect(subject).to receive(:uploads_dir).and_return(uploads_dir)
+ expect(subject).to receive(:move_folders).with(uploads_dir, 'old_path', 'new_path')
+
+ subject.move_uploads('old_path', 'new_path')
+ end
+
+ it "doesn't move uploads when they are stored in object storage" do
+ expect(subject).to receive(:file_storage?).and_return(false)
+ expect(subject).not_to receive(:move_folders)
+
+ subject.move_uploads('old_path', 'new_path')
+ end
+ end
+
+ describe '#move_folders' do
+ let(:test_dir) { File.join(Rails.root, 'tmp', 'tests', 'rename_reserved_paths') }
+ let(:uploads_dir) { File.join(test_dir, 'public', 'uploads') }
+
+ before do
+ FileUtils.remove_dir(test_dir) if File.directory?(test_dir)
+ FileUtils.mkdir_p(uploads_dir)
+ allow(subject).to receive(:uploads_dir).and_return(uploads_dir)
+ end
+
+ it 'moves a folder with files' do
+ source = File.join(uploads_dir, 'parent-group', 'sub-group')
+ FileUtils.mkdir_p(source)
+ destination = File.join(uploads_dir, 'parent-group', 'moved-group')
+ FileUtils.touch(File.join(source, 'test.txt'))
+ expected_file = File.join(destination, 'test.txt')
+
+ subject.move_folders(uploads_dir, File.join('parent-group', 'sub-group'), File.join('parent-group', 'moved-group'))
+
+ expect(File.exist?(expected_file)).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
new file mode 100644
index 00000000000..a25c5da488a
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_namespaces_spec.rb
@@ -0,0 +1,171 @@
+require 'spec_helper'
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameNamespaces do
+ let(:migration) { FakeRenameReservedPathMigrationV1.new }
+ let(:subject) { described_class.new(['the-path'], migration) }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ def migration_namespace(namespace)
+ Gitlab::Database::RenameReservedPathsMigration::V1::MigrationClasses::
+ Namespace.find(namespace.id)
+ end
+
+ describe '#namespaces_for_paths' do
+ context 'nested namespaces' do
+ let(:subject) { described_class.new(['parent/the-Path'], migration) }
+
+ it 'includes the namespace' do
+ parent = create(:namespace, path: 'parent')
+ child = create(:namespace, path: 'the-path', parent: parent)
+
+ found_ids = subject.namespaces_for_paths(type: :child).
+ map(&:id)
+ expect(found_ids).to contain_exactly(child.id)
+ end
+ end
+
+ context 'for child namespaces' do
+ it 'only returns child namespaces with the correct path' do
+ _root_namespace = create(:namespace, path: 'THE-path')
+ _other_path = create(:namespace,
+ path: 'other',
+ parent: create(:namespace))
+ namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :child).
+ map(&:id)
+ expect(found_ids).to contain_exactly(namespace.id)
+ end
+ end
+
+ context 'for top levelnamespaces' do
+ it 'only returns child namespaces with the correct path' do
+ root_namespace = create(:namespace, path: 'the-path')
+ _other_path = create(:namespace, path: 'other')
+ _child_namespace = create(:namespace,
+ path: 'the-path',
+ parent: create(:namespace))
+
+ found_ids = subject.namespaces_for_paths(type: :top_level).
+ map(&:id)
+ expect(found_ids).to contain_exactly(root_namespace.id)
+ end
+ end
+ end
+
+ describe '#move_repositories' do
+ let(:namespace) { create(:group, name: 'hello-group') }
+ it 'moves a project for a namespace' do
+ create(:project, namespace: namespace, path: 'hello-project')
+ expected_path = File.join(TestEnv.repos_path, 'bye-group', 'hello-project.git')
+
+ subject.move_repositories(namespace, 'hello-group', 'bye-group')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+
+ it 'moves a namespace in a subdirectory correctly' do
+ child_namespace = create(:group, name: 'sub-group', parent: namespace)
+ create(:project, namespace: child_namespace, path: 'hello-project')
+
+ expected_path = File.join(TestEnv.repos_path, 'hello-group', 'renamed-sub-group', 'hello-project.git')
+
+ subject.move_repositories(child_namespace, 'hello-group/sub-group', 'hello-group/renamed-sub-group')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+
+ it 'moves a parent namespace with subdirectories' do
+ child_namespace = create(:group, name: 'sub-group', parent: namespace)
+ create(:project, namespace: child_namespace, path: 'hello-project')
+ expected_path = File.join(TestEnv.repos_path, 'renamed-group', 'sub-group', 'hello-project.git')
+
+ subject.move_repositories(child_namespace, 'hello-group', 'renamed-group')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+ end
+
+ describe "#child_ids_for_parent" do
+ it "collects child ids for all levels" do
+ parent = create(:namespace)
+ first_child = create(:namespace, parent: parent)
+ second_child = create(:namespace, parent: parent)
+ third_child = create(:namespace, parent: second_child)
+ all_ids = [parent.id, first_child.id, second_child.id, third_child.id]
+
+ collected_ids = subject.child_ids_for_parent(parent, ids: [parent.id])
+
+ expect(collected_ids).to contain_exactly(*all_ids)
+ end
+ end
+
+ describe "#rename_namespace" do
+ let(:namespace) { create(:namespace, path: 'the-path') }
+
+ it 'renames paths & routes for the namespace' do
+ expect(subject).to receive(:rename_path_for_routable).
+ with(namespace).
+ and_call_original
+
+ subject.rename_namespace(namespace)
+
+ expect(namespace.reload.path).to eq('the-path0')
+ end
+
+ it "moves the the repository for a project in the namespace" do
+ create(:project, namespace: namespace, path: "the-path-project")
+ expected_repo = File.join(TestEnv.repos_path, "the-path0", "the-path-project.git")
+
+ subject.rename_namespace(namespace)
+
+ expect(File.directory?(expected_repo)).to be(true)
+ end
+
+ it "moves the uploads for the namespace" do
+ expect(subject).to receive(:move_uploads).with("the-path", "the-path0")
+
+ subject.rename_namespace(namespace)
+ end
+
+ it "moves the pages for the namespace" do
+ expect(subject).to receive(:move_pages).with("the-path", "the-path0")
+
+ subject.rename_namespace(namespace)
+ end
+
+ it 'invalidates the markdown cache of related projects' do
+ project = create(:empty_project, namespace: namespace, path: "the-path-project")
+
+ expect(subject).to receive(:remove_cached_html_for_projects).with([project.id])
+
+ subject.rename_namespace(namespace)
+ end
+ end
+
+ describe '#rename_namespaces' do
+ let!(:top_level_namespace) { create(:namespace, path: 'the-path') }
+ let!(:child_namespace) do
+ create(:namespace, path: 'the-path', parent: create(:namespace))
+ end
+
+ it 'renames top level namespaces the namespace' do
+ expect(subject).to receive(:rename_namespace).
+ with(migration_namespace(top_level_namespace))
+
+ subject.rename_namespaces(type: :top_level)
+ end
+
+ it 'renames child namespaces' do
+ expect(subject).to receive(:rename_namespace).
+ with(migration_namespace(child_namespace))
+
+ subject.rename_namespaces(type: :child)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
new file mode 100644
index 00000000000..59e8de2712d
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1/rename_projects_spec.rb
@@ -0,0 +1,102 @@
+require 'spec_helper'
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1::RenameProjects do
+ let(:migration) { FakeRenameReservedPathMigrationV1.new }
+ let(:subject) { described_class.new(['the-path'], migration) }
+
+ before do
+ allow(migration).to receive(:say)
+ end
+
+ describe '#projects_for_paths' do
+ it 'searches using nested paths' do
+ namespace = create(:namespace, path: 'hello')
+ project = create(:empty_project, path: 'THE-path', namespace: namespace)
+
+ result_ids = described_class.new(['Hello/the-path'], migration).
+ projects_for_paths.map(&:id)
+
+ expect(result_ids).to contain_exactly(project.id)
+ end
+
+ it 'includes the correct projects' do
+ project = create(:empty_project, path: 'THE-path')
+ _other_project = create(:empty_project)
+
+ result_ids = subject.projects_for_paths.map(&:id)
+
+ expect(result_ids).to contain_exactly(project.id)
+ end
+ end
+
+ describe '#rename_projects' do
+ let!(:projects) { create_list(:empty_project, 2, path: 'the-path') }
+
+ it 'renames each project' do
+ expect(subject).to receive(:rename_project).twice
+
+ subject.rename_projects
+ end
+
+ it 'invalidates the markdown cache of related projects' do
+ expect(subject).to receive(:remove_cached_html_for_projects).
+ with(projects.map(&:id))
+
+ subject.rename_projects
+ end
+ end
+
+ describe '#rename_project' do
+ let(:project) do
+ create(:empty_project,
+ path: 'the-path',
+ namespace: create(:namespace, path: 'known-parent' ))
+ end
+
+ it 'renames path & route for the project' do
+ expect(subject).to receive(:rename_path_for_routable).
+ with(project).
+ and_call_original
+
+ subject.rename_project(project)
+
+ expect(project.reload.path).to eq('the-path0')
+ end
+
+ it 'moves the wiki & the repo' do
+ expect(subject).to receive(:move_repository).
+ with(project, 'known-parent/the-path.wiki', 'known-parent/the-path0.wiki')
+ expect(subject).to receive(:move_repository).
+ with(project, 'known-parent/the-path', 'known-parent/the-path0')
+
+ subject.rename_project(project)
+ end
+
+ it 'moves uploads' do
+ expect(subject).to receive(:move_uploads).
+ with('known-parent/the-path', 'known-parent/the-path0')
+
+ subject.rename_project(project)
+ end
+
+ it 'moves pages' do
+ expect(subject).to receive(:move_pages).
+ with('known-parent/the-path', 'known-parent/the-path0')
+
+ subject.rename_project(project)
+ end
+ end
+
+ describe '#move_repository' do
+ let(:known_parent) { create(:namespace, path: 'known-parent') }
+ let(:project) { create(:project, path: 'the-path', namespace: known_parent) }
+
+ it 'moves the repository for a project' do
+ expected_path = File.join(TestEnv.repos_path, 'known-parent', 'new-repo.git')
+
+ subject.move_repository(project, 'known-parent/the-path', 'known-parent/new-repo')
+
+ expect(File.directory?(expected_path)).to be(true)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
new file mode 100644
index 00000000000..f8cc1eb91ec
--- /dev/null
+++ b/spec/lib/gitlab/database/rename_reserved_paths_migration/v1_spec.rb
@@ -0,0 +1,54 @@
+require 'spec_helper'
+
+shared_examples 'renames child namespaces' do |type|
+ it 'renames namespaces' do
+ rename_namespaces = double
+ expect(described_class::RenameNamespaces).
+ to receive(:new).with(['first-path', 'second-path'], subject).
+ and_return(rename_namespaces)
+ expect(rename_namespaces).to receive(:rename_namespaces).
+ with(type: :child)
+
+ subject.rename_wildcard_paths(['first-path', 'second-path'])
+ end
+end
+
+describe Gitlab::Database::RenameReservedPathsMigration::V1 do
+ let(:subject) { FakeRenameReservedPathMigrationV1.new }
+
+ before do
+ allow(subject).to receive(:say)
+ end
+
+ describe '#rename_child_paths' do
+ it_behaves_like 'renames child namespaces'
+ end
+
+ describe '#rename_wildcard_paths' do
+ it_behaves_like 'renames child namespaces'
+
+ it 'should rename projects' do
+ rename_projects = double
+ expect(described_class::RenameProjects).
+ to receive(:new).with(['the-path'], subject).
+ and_return(rename_projects)
+
+ expect(rename_projects).to receive(:rename_projects)
+
+ subject.rename_wildcard_paths(['the-path'])
+ end
+ end
+
+ describe '#rename_root_paths' do
+ it 'should rename namespaces' do
+ rename_namespaces = double
+ expect(described_class::RenameNamespaces).
+ to receive(:new).with(['the-path'], subject).
+ and_return(rename_namespaces)
+ expect(rename_namespaces).to receive(:rename_namespaces).
+ with(type: :top_level)
+
+ subject.rename_root_paths('the-path')
+ end
+ end
+end
diff --git a/spec/lib/gitlab/email/receiver_spec.rb b/spec/lib/gitlab/email/receiver_spec.rb
index 2a86b427806..f127e45ae6a 100644
--- a/spec/lib/gitlab/email/receiver_spec.rb
+++ b/spec/lib/gitlab/email/receiver_spec.rb
@@ -7,9 +7,17 @@ describe Gitlab::Email::Receiver, lib: true do
context "when we cannot find a capable handler" do
let(:email_raw) { fixture_file('emails/valid_reply.eml').gsub(mail_key, "!!!") }
- it "raises a UnknownIncomingEmail" do
+ it "raises an UnknownIncomingEmail error" do
expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
end
+
+ context "and the email contains no references header" do
+ let(:email_raw) { fixture_file("emails/auto_reply.eml").gsub(mail_key, "!!!") }
+
+ it "raises an UnknownIncomingEmail error" do
+ expect { receiver.execute }.to raise_error(Gitlab::Email::UnknownIncomingEmail)
+ end
+ end
end
context "when the email is blank" do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index f88653cb1fe..1b78910fa3c 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1074,20 +1074,8 @@ describe Gitlab::Git::Repository, seed_helper: true do
end
describe '#branch_count' do
- before(:each) do
- valid_ref = double(:ref)
- invalid_ref = double(:ref)
-
- allow(valid_ref).to receive_messages(name: 'master', target: double(:target))
-
- allow(invalid_ref).to receive_messages(name: 'bad-branch')
- allow(invalid_ref).to receive(:target) { raise Rugged::ReferenceError }
-
- allow(repository.rugged).to receive_messages(branches: [valid_ref, invalid_ref])
- end
-
it 'returns the number of branches' do
- expect(repository.branch_count).to eq(1)
+ expect(repository.branch_count).to eq(9)
end
end
diff --git a/spec/lib/gitlab/regex_spec.rb b/spec/lib/gitlab/regex_spec.rb
index 127cd8c78d8..72e947f2cc2 100644
--- a/spec/lib/gitlab/regex_spec.rb
+++ b/spec/lib/gitlab/regex_spec.rb
@@ -45,8 +45,8 @@ describe Gitlab::Regex, lib: true do
it { is_expected.not_to match('foo-') }
end
- describe 'FULL_NAMESPACE_REGEX_STR' do
- subject { %r{\A#{Gitlab::Regex::FULL_NAMESPACE_REGEX_STR}\z} }
+ describe '.full_namespace_regex' do
+ subject { described_class.full_namespace_regex }
it { is_expected.to match('gitlab.org') }
it { is_expected.to match('gitlab.org/gitlab-git') }
diff --git a/spec/lib/gitlab/request_profiler_spec.rb b/spec/lib/gitlab/request_profiler_spec.rb
new file mode 100644
index 00000000000..ae9c06ebb7d
--- /dev/null
+++ b/spec/lib/gitlab/request_profiler_spec.rb
@@ -0,0 +1,27 @@
+require 'spec_helper'
+
+describe Gitlab::RequestProfiler, lib: true do
+ describe '.profile_token' do
+ it 'returns a token' do
+ expect(described_class.profile_token).to be_present
+ end
+
+ it 'caches the token' do
+ expect(Rails.cache).to receive(:fetch).with('profile-token')
+
+ described_class.profile_token
+ end
+ end
+
+ describe '.remove_all_profiles' do
+ it 'removes Gitlab::RequestProfiler::PROFILES_DIR directory' do
+ dir = described_class::PROFILES_DIR
+ FileUtils.mkdir_p(dir)
+
+ expect(Dir.exist?(dir)).to be true
+
+ described_class.remove_all_profiles
+ expect(Dir.exist?(dir)).to be false
+ end
+ end
+end
diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb
index 611cdbbc865..2b27ff66c09 100644
--- a/spec/lib/gitlab/user_access_spec.rb
+++ b/spec/lib/gitlab/user_access_spec.rb
@@ -87,10 +87,10 @@ describe Gitlab::UserAccess, lib: true do
expect(access.can_push_to_branch?(branch.name)).to be_falsey
end
- it 'returns true if branch does not exist and user has permission to merge' do
+ it 'returns false if branch does not exist' do
project.team << [user, :developer]
- expect(access.can_push_to_branch?(not_existing_branch.name)).to be_truthy
+ expect(access.can_push_to_branch?(not_existing_branch.name)).to be_falsey
end
end
diff --git a/spec/models/concerns/cache_markdown_field_spec.rb b/spec/models/concerns/cache_markdown_field_spec.rb
index 4edafbc4e32..40bbb10eaac 100644
--- a/spec/models/concerns/cache_markdown_field_spec.rb
+++ b/spec/models/concerns/cache_markdown_field_spec.rb
@@ -170,6 +170,12 @@ describe CacheMarkdownField do
is_expected.to be_truthy
end
+
+ it 'returns false if the markdown field is set but the html is not' do
+ thing.foo_html = nil
+
+ is_expected.to be_falsy
+ end
end
describe '#refresh_markdown_cache!' do
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 8ffde6f7fbb..a11805926cc 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -57,6 +57,32 @@ describe Group, models: true do
it { is_expected.not_to validate_presence_of :owner }
it { is_expected.to validate_presence_of :two_factor_grace_period }
it { is_expected.to validate_numericality_of(:two_factor_grace_period).is_greater_than_or_equal_to(0) }
+
+ describe 'path validation' do
+ it 'rejects paths reserved on the root namespace when the group has no parent' do
+ group = build(:group, path: 'api')
+
+ expect(group).not_to be_valid
+ end
+
+ it 'allows root paths when the group has a parent' do
+ group = build(:group, path: 'api', parent: create(:group))
+
+ expect(group).to be_valid
+ end
+
+ it 'rejects any wildcard paths when not a top level group' do
+ group = build(:group, path: 'tree', parent: create(:group))
+
+ expect(group).not_to be_valid
+ end
+
+ it 'rejects reserved group paths' do
+ group = build(:group, path: 'activity', parent: create(:group))
+
+ expect(group).not_to be_valid
+ end
+ end
end
describe '.visible_to_user' do
diff --git a/spec/models/namespace_spec.rb b/spec/models/namespace_spec.rb
index e406d0a16bd..8624616316c 100644
--- a/spec/models/namespace_spec.rb
+++ b/spec/models/namespace_spec.rb
@@ -34,6 +34,13 @@ describe Namespace, models: true do
let(:group) { build(:group, :nested, path: 'tree') }
it { expect(group).not_to be_valid }
+
+ it 'rejects nested paths' do
+ parent = create(:group, :nested, path: 'environments')
+ namespace = build(:project, path: 'folders', namespace: parent)
+
+ expect(namespace).not_to be_valid
+ end
end
context 'top-level group' do
@@ -47,6 +54,7 @@ describe Namespace, models: true do
describe "Respond to" do
it { is_expected.to respond_to(:human_name) }
it { is_expected.to respond_to(:to_param) }
+ it { is_expected.to respond_to(:has_parent?) }
end
describe '#to_param' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 92d420337f9..49455303096 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -253,6 +253,34 @@ describe Project, models: true do
expect(new_project.errors.full_messages.first).to eq('The project is still being deleted. Please try again later.')
end
end
+
+ describe 'path validation' do
+ it 'allows paths reserved on the root namespace' do
+ project = build(:project, path: 'api')
+
+ expect(project).to be_valid
+ end
+
+ it 'rejects paths reserved on another level' do
+ project = build(:project, path: 'tree')
+
+ expect(project).not_to be_valid
+ end
+
+ it 'rejects nested paths' do
+ parent = create(:group, :nested, path: 'environments')
+ project = build(:project, path: 'folders', namespace: parent)
+
+ expect(project).not_to be_valid
+ end
+
+ it 'allows a reserved group name' do
+ parent = create(:group)
+ project = build(:project, path: 'avatar', namespace: parent)
+
+ expect(project).to be_valid
+ end
+ end
end
describe 'default_scope' do
@@ -781,17 +809,14 @@ describe Project, models: true do
let(:project) { create(:empty_project) }
- context 'When avatar file is uploaded' do
- before do
- project.update_columns(avatar: 'uploads/avatar.png')
- allow(project.avatar).to receive(:present?) { true }
- end
+ context 'when avatar file is uploaded' do
+ let(:project) { create(:empty_project, :with_avatar) }
- let(:avatar_path) do
- "/uploads/project/avatar/#{project.id}/uploads/avatar.png"
- end
+ it 'creates a correct avatar path' do
+ avatar_path = "/uploads/project/avatar/#{project.id}/dk.png"
- it { should eq "http://#{Gitlab.config.gitlab.host}#{avatar_path}" }
+ expect(project.avatar_url).to eq("http://#{Gitlab.config.gitlab.host}#{avatar_path}")
+ end
end
context 'When avatar file in git' do
diff --git a/spec/models/repository_spec.rb b/spec/models/repository_spec.rb
index f6846cc1b2f..5216764a82d 100644
--- a/spec/models/repository_spec.rb
+++ b/spec/models/repository_spec.rb
@@ -1379,12 +1379,22 @@ describe Repository, models: true do
describe '#branch_count' do
it 'returns the number of branches' do
expect(repository.branch_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.branches.count
+
+ expect(repository.branch_count).to eq(rugged_count)
end
end
describe '#tag_count' do
it 'returns the number of tags' do
expect(repository.tag_count).to be_an(Integer)
+
+ # NOTE: Until rugged goes away, make sure rugged and gitaly are in sync
+ rugged_count = repository.raw_repository.rugged.tags.count
+
+ expect(repository.tag_count).to eq(rugged_count)
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index 0bcebc27598..1c2df4c9d97 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -97,6 +97,18 @@ describe User, models: true do
expect(user.errors.values).to eq [['dashboard is a reserved name']]
end
+ it 'allows child names' do
+ user = build(:user, username: 'avatar')
+
+ expect(user).to be_valid
+ end
+
+ it 'allows wildcard names' do
+ user = build(:user, username: 'blob')
+
+ expect(user).to be_valid
+ end
+
it 'validates uniqueness' do
expect(subject).to validate_uniqueness_of(:username).case_insensitive
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index c4bff1647b5..16e5efb2f5b 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -434,6 +434,19 @@ describe API::MergeRequests do
expect(json_response['title']).to eq('Test merge_request')
end
+ it 'returns 422 when target project has disabled merge requests' do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ post api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test',
+ target_branch: 'master',
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: project.id
+
+ expect(response).to have_http_status(422)
+ end
+
it "returns 400 when source_branch is missing" do
post api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
diff --git a/spec/requests/api/v3/merge_requests_spec.rb b/spec/requests/api/v3/merge_requests_spec.rb
index 6c2950a6e6f..f6ff96be566 100644
--- a/spec/requests/api/v3/merge_requests_spec.rb
+++ b/spec/requests/api/v3/merge_requests_spec.rb
@@ -338,6 +338,19 @@ describe API::MergeRequests do
expect(json_response['title']).to eq('Test merge_request')
end
+ it "returns 422 when target project has disabled merge requests" do
+ project.project_feature.update(merge_requests_access_level: 0)
+
+ post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
+ title: 'Test',
+ target_branch: "master",
+ source_branch: 'markdown',
+ author: user2,
+ target_project_id: project.id
+
+ expect(response).to have_http_status(422)
+ end
+
it "returns 400 when source_branch is missing" do
post v3_api("/projects/#{fork_project.id}/merge_requests", user2),
title: 'Test merge_request', target_branch: "master", author: user2, target_project_id: project.id
diff --git a/spec/requests/request_profiler_spec.rb b/spec/requests/request_profiler_spec.rb
new file mode 100644
index 00000000000..51fbfecec4b
--- /dev/null
+++ b/spec/requests/request_profiler_spec.rb
@@ -0,0 +1,44 @@
+require 'spec_helper'
+
+describe 'Request Profiler' do
+ let(:user) { create(:user) }
+
+ shared_examples 'profiling a request' do
+ before do
+ allow(Rails).to receive(:cache).and_return(ActiveSupport::Cache::MemoryStore.new)
+ allow(RubyProf::Profile).to receive(:profile) do |&blk|
+ blk.call
+ RubyProf::Profile.new
+ end
+ end
+
+ it 'creates a profile of the request' do
+ project = create(:project, namespace: user.namespace)
+ time = Time.now
+ path = "/#{project.path_with_namespace}"
+
+ Timecop.freeze(time) do
+ get path, nil, 'X-Profile-Token' => Gitlab::RequestProfiler.profile_token
+ end
+
+ profile_path = "#{Gitlab.config.shared.path}/tmp/requests_profiles/#{path.tr('/', '|')}_#{time.to_i}.html"
+ expect(File.exist?(profile_path)).to be true
+ end
+
+ after do
+ Gitlab::RequestProfiler.remove_all_profiles
+ end
+ end
+
+ context "when user is logged-in" do
+ before do
+ login_as(user)
+ end
+
+ include_examples 'profiling a request'
+ end
+
+ context "when user is not logged-in" do
+ include_examples 'profiling a request'
+ end
+end
diff --git a/spec/routing/admin_routing_spec.rb b/spec/routing/admin_routing_spec.rb
index 99c44bde151..e5fc0b676af 100644
--- a/spec/routing/admin_routing_spec.rb
+++ b/spec/routing/admin_routing_spec.rb
@@ -71,13 +71,15 @@ describe Admin::ProjectsController, "routing" do
end
end
-# admin_hook_test GET /admin/hooks/:hook_id/test(.:format) admin/hooks#test
+# admin_hook_test GET /admin/hooks/:id/test(.:format) admin/hooks#test
# admin_hooks GET /admin/hooks(.:format) admin/hooks#index
# POST /admin/hooks(.:format) admin/hooks#create
# admin_hook DELETE /admin/hooks/:id(.:format) admin/hooks#destroy
+# PUT /admin/hooks/:id(.:format) admin/hooks#update
+# edit_admin_hook GET /admin/hooks/:id(.:format) admin/hooks#edit
describe Admin::HooksController, "routing" do
it "to #test" do
- expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', hook_id: '1')
+ expect(get("/admin/hooks/1/test")).to route_to('admin/hooks#test', id: '1')
end
it "to #index" do
@@ -88,6 +90,14 @@ describe Admin::HooksController, "routing" do
expect(post("/admin/hooks")).to route_to('admin/hooks#create')
end
+ it "to #edit" do
+ expect(get("/admin/hooks/1/edit")).to route_to('admin/hooks#edit', id: '1')
+ end
+
+ it "to #update" do
+ expect(put("/admin/hooks/1")).to route_to('admin/hooks#update', id: '1')
+ end
+
it "to #destroy" do
expect(delete("/admin/hooks/1")).to route_to('admin/hooks#destroy', id: '1')
end
diff --git a/spec/routing/project_routing_spec.rb b/spec/routing/project_routing_spec.rb
index a3de022d242..163df072cf6 100644
--- a/spec/routing/project_routing_spec.rb
+++ b/spec/routing/project_routing_spec.rb
@@ -340,14 +340,16 @@ describe 'project routing' do
# test_project_hook GET /:project_id/hooks/:id/test(.:format) hooks#test
# project_hooks GET /:project_id/hooks(.:format) hooks#index
# POST /:project_id/hooks(.:format) hooks#create
- # project_hook DELETE /:project_id/hooks/:id(.:format) hooks#destroy
+ # edit_project_hook GET /:project_id/hooks/:id/edit(.:format) hooks#edit
+ # project_hook PUT /:project_id/hooks/:id(.:format) hooks#update
+ # DELETE /:project_id/hooks/:id(.:format) hooks#destroy
describe Projects::HooksController, 'routing' do
it 'to #test' do
expect(get('/gitlab/gitlabhq/hooks/1/test')).to route_to('projects/hooks#test', namespace_id: 'gitlab', project_id: 'gitlabhq', id: '1')
end
it_behaves_like 'RESTful project resources' do
- let(:actions) { [:index, :create, :destroy] }
+ let(:actions) { [:index, :create, :destroy, :edit, :update] }
let(:controller) { 'hooks' }
end
end
diff --git a/spec/serializers/deployment_entity_spec.rb b/spec/serializers/deployment_entity_spec.rb
index 95eca5463eb..69355bcde42 100644
--- a/spec/serializers/deployment_entity_spec.rb
+++ b/spec/serializers/deployment_entity_spec.rb
@@ -3,25 +3,23 @@ require 'spec_helper'
describe DeploymentEntity do
let(:user) { create(:user) }
let(:request) { double('request') }
+ let(:deployment) { create(:deployment) }
+ let(:entity) { described_class.new(deployment, request: request) }
+ subject { entity.as_json }
before do
allow(request).to receive(:user).and_return(user)
end
- let(:entity) do
- described_class.new(deployment, request: request)
- end
-
- let(:deployment) { create(:deployment) }
-
- subject { entity.as_json }
-
it 'exposes internal deployment id' do
expect(subject).to include(:iid)
end
it 'exposes nested information about branch' do
expect(subject[:ref][:name]).to eq 'master'
- expect(subject[:ref][:ref_path]).not_to be_empty
+ end
+
+ it 'exposes creation date' do
+ expect(subject).to include(:created_at)
end
end
diff --git a/spec/serializers/status_entity_spec.rb b/spec/serializers/status_entity_spec.rb
index c94902dbab8..3964b998084 100644
--- a/spec/serializers/status_entity_spec.rb
+++ b/spec/serializers/status_entity_spec.rb
@@ -18,6 +18,12 @@ describe StatusEntity do
it 'contains status details' do
expect(subject).to include :text, :icon, :favicon, :label, :group
expect(subject).to include :has_details, :details_path
+ expect(subject[:favicon]).to eq('/assets/ci_favicons/favicon_status_success.ico')
+ end
+
+ it 'contains a dev namespaced favicon if dev env' do
+ allow(Rails.env).to receive(:development?) { true }
+ expect(entity.as_json[:favicon]).to eq('/assets/ci_favicons/dev/favicon_status_success.ico')
end
end
end
diff --git a/spec/services/merge_requests/build_service_spec.rb b/spec/services/merge_requests/build_service_spec.rb
index be9f9ea2dec..6f9d1208b1d 100644
--- a/spec/services/merge_requests/build_service_spec.rb
+++ b/spec/services/merge_requests/build_service_spec.rb
@@ -261,6 +261,16 @@ describe MergeRequests::BuildService, services: true do
end
end
+ context 'upstream project has disabled merge requests' do
+ let(:upstream_project) { create(:empty_project, :merge_requests_disabled) }
+ let(:project) { create(:empty_project, forked_from_project: upstream_project) }
+ let(:commits) { Commit.decorate([commit_1], project) }
+
+ it 'sets target project correctly' do
+ expect(merge_request.target_project).to eq(project)
+ end
+ end
+
context 'target_project is set and accessible by current_user' do
let(:target_project) { create(:project, :public, :repository)}
let(:commits) { Commit.decorate([commit_1], project) }
diff --git a/spec/support/fake_migration_classes.rb b/spec/support/fake_migration_classes.rb
new file mode 100644
index 00000000000..3de0460c3ca
--- /dev/null
+++ b/spec/support/fake_migration_classes.rb
@@ -0,0 +1,3 @@
+class FakeRenameReservedPathMigrationV1 < ActiveRecord::Migration
+ include Gitlab::Database::RenameReservedPathsMigration::V1
+end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 5c8ee8d62f5..0b3c6169c9b 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -39,7 +39,8 @@ module TestEnv
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => '6d85bb69'
+ 'add-ipython-files' => '6d85bb69',
+ 'add-pdf-file' => 'e774ebd3'
}.freeze
# gitlab-test-fork is a fork of gitlab-fork, but we don't necessarily
diff --git a/spec/tasks/gitlab/backup_rake_spec.rb b/spec/tasks/gitlab/backup_rake_spec.rb
index 0a4a6ed8145..df2f2ce95e6 100644
--- a/spec/tasks/gitlab/backup_rake_spec.rb
+++ b/spec/tasks/gitlab/backup_rake_spec.rb
@@ -230,11 +230,13 @@ describe 'gitlab:app namespace rake task' do
before do
FileUtils.mkdir('tmp/tests/default_storage')
FileUtils.mkdir('tmp/tests/custom_storage')
+ gitaly_address = Gitlab.config.repositories.storages.default.gitaly_address
storages = {
- 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage') },
- 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage') }
+ 'default' => { 'path' => Settings.absolute('tmp/tests/default_storage'), 'gitaly_address' => gitaly_address },
+ 'custom' => { 'path' => Settings.absolute('tmp/tests/custom_storage'), 'gitaly_address' => gitaly_address }
}
allow(Gitlab.config.repositories).to receive(:storages).and_return(storages)
+ Gitlab::GitalyClient.configure_channels
# Create the projects now, after mocking the settings but before doing the backup
project_a
diff --git a/spec/validators/dynamic_path_validator_spec.rb b/spec/validators/dynamic_path_validator_spec.rb
new file mode 100644
index 00000000000..b114bfc1bca
--- /dev/null
+++ b/spec/validators/dynamic_path_validator_spec.rb
@@ -0,0 +1,266 @@
+require 'spec_helper'
+
+describe DynamicPathValidator do
+ let(:validator) { described_class.new(attributes: [:path]) }
+
+ # Pass in a full path to remove the format segment:
+ # `/ci/lint(.:format)` -> `/ci/lint`
+ def without_format(path)
+ path.split('(', 2)[0]
+ end
+
+ # Pass in a full path and get the last segment before a wildcard
+ # That's not a parameter
+ # `/*namespace_id/:project_id/builds/artifacts/*ref_name_and_path`
+ # -> 'builds/artifacts'
+ def path_before_wildcard(path)
+ path = path.gsub(STARTING_WITH_NAMESPACE, "")
+ path_segments = path.split('/').reject(&:empty?)
+ wildcard_index = path_segments.index { |segment| parameter?(segment) }
+
+ segments_before_wildcard = path_segments[0..wildcard_index - 1]
+
+ segments_before_wildcard.join('/')
+ end
+
+ def parameter?(segment)
+ segment =~ /[*:]/
+ end
+
+ # If the path is reserved. Then no conflicting paths can# be created for any
+ # route using this reserved word.
+ #
+ # Both `builds/artifacts` & `build` are covered by reserving the word
+ # `build`
+ def wildcards_include?(path)
+ described_class::WILDCARD_ROUTES.include?(path) ||
+ described_class::WILDCARD_ROUTES.include?(path.split('/').first)
+ end
+
+ def failure_message(missing_words, constant_name, migration_helper)
+ missing_words = Array(missing_words)
+ <<-MSG
+ Found new routes that could cause conflicts with existing namespaced routes
+ for groups or projects.
+
+ Add <#{missing_words.join(', ')}> to `DynamicPathValidator::#{constant_name}
+ to make sure no projects or namespaces can be created with those paths.
+
+ To rename any existing records with those paths you can use the
+ `Gitlab::Database::RenameReservedpathsMigration::<VERSION>.#{migration_helper}`
+ migration helper.
+
+ Make sure to make a note of the renamed records in the release blog post.
+
+ MSG
+ end
+
+ let(:all_routes) do
+ Rails.application.routes.routes.routes.
+ map { |r| r.path.spec.to_s }
+ end
+
+ let(:routes_without_format) { all_routes.map { |path| without_format(path) } }
+
+ # Routes not starting with `/:` or `/*`
+ # all routes not starting with a param
+ let(:routes_not_starting_in_wildcard) { routes_without_format.select { |p| p !~ %r{^/[:*]} } }
+
+ let(:top_level_words) do
+ routes_not_starting_in_wildcard.map do |route|
+ route.split('/')[1]
+ end.compact.uniq
+ end
+
+ # All routes that start with a namespaced path, that have 1 or more
+ # path-segments before having another wildcard parameter.
+ # - Starting with paths:
+ # - `/*namespace_id/:project_id/`
+ # - `/*namespace_id/:id/`
+ # - Followed by one or more path-parts not starting with `:` or `*`
+ # - Followed by a path-part that includes a wildcard parameter `*`
+ # At the time of writing these routes match: http://rubular.com/r/Rv2pDE5Dvw
+ STARTING_WITH_NAMESPACE = %r{^/\*namespace_id/:(project_)?id}
+ NON_PARAM_PARTS = %r{[^:*][a-z\-_/]*}
+ ANY_OTHER_PATH_PART = %r{[a-z\-_/:]*}
+ WILDCARD_SEGMENT = %r{\*}
+ let(:namespaced_wildcard_routes) do
+ routes_without_format.select do |p|
+ p =~ %r{#{STARTING_WITH_NAMESPACE}/#{NON_PARAM_PARTS}/#{ANY_OTHER_PATH_PART}#{WILDCARD_SEGMENT}}
+ end
+ end
+
+ # This will return all paths that are used in a namespaced route
+ # before another wildcard path:
+ #
+ # /*namespace_id/:project_id/builds/artifacts/*ref_name_and_path
+ # /*namespace_id/:project_id/info/lfs/objects/*oid
+ # /*namespace_id/:project_id/commits/*id
+ # /*namespace_id/:project_id/builds/:build_id/artifacts/file/*path
+ # -> ['builds/artifacts', 'info/lfs/objects', 'commits', 'artifacts/file']
+ let(:all_wildcard_paths) do
+ namespaced_wildcard_routes.map do |route|
+ path_before_wildcard(route)
+ end.uniq
+ end
+
+ STARTING_WITH_GROUP = %r{^/groups/\*(group_)?id/}
+ let(:group_routes) do
+ routes_without_format.select do |path|
+ path =~ STARTING_WITH_GROUP
+ end
+ end
+
+ let(:paths_after_group_id) do
+ group_routes.map do |route|
+ route.gsub(STARTING_WITH_GROUP, '').split('/').first
+ end.uniq
+ end
+
+ describe 'TOP_LEVEL_ROUTES' do
+ it 'includes all the top level namespaces' do
+ failure_block = lambda do
+ missing_words = top_level_words - described_class::TOP_LEVEL_ROUTES
+ failure_message(missing_words, 'TOP_LEVEL_ROUTES', 'rename_root_paths')
+ end
+
+ expect(described_class::TOP_LEVEL_ROUTES)
+ .to include(*top_level_words), failure_block
+ end
+ end
+
+ describe 'GROUP_ROUTES' do
+ it "don't contain a second wildcard" do
+ failure_block = lambda do
+ missing_words = paths_after_group_id - described_class::GROUP_ROUTES
+ failure_message(missing_words, 'GROUP_ROUTES', 'rename_child_paths')
+ end
+
+ expect(described_class::GROUP_ROUTES)
+ .to include(*paths_after_group_id), failure_block
+ end
+ end
+
+ describe 'WILDCARD_ROUTES' do
+ it 'includes all paths that can be used after a namespace/project path' do
+ aggregate_failures do
+ all_wildcard_paths.each do |path|
+ expect(wildcards_include?(path))
+ .to be(true), failure_message(path, 'WILDCARD_ROUTES', 'rename_wildcard_paths')
+ end
+ end
+ end
+ end
+
+ describe '.without_reserved_wildcard_paths_regex' do
+ subject { described_class.without_reserved_wildcard_paths_regex }
+
+ it 'rejects paths starting with a reserved top level' do
+ expect(subject).not_to match('dashboard/hello/world')
+ expect(subject).not_to match('dashboard')
+ end
+
+ it 'matches valid paths with a toplevel word in a different place' do
+ expect(subject).to match('parent/dashboard/project-path')
+ end
+
+ it 'rejects paths containing a wildcard reserved word' do
+ expect(subject).not_to match('hello/edit')
+ expect(subject).not_to match('hello/edit/in-the-middle')
+ expect(subject).not_to match('foo/bar1/refs/master/logs_tree')
+ end
+
+ it 'matches valid paths' do
+ expect(subject).to match('parent/child/project-path')
+ end
+ end
+
+ describe '.regex_excluding_child_paths' do
+ let(:subject) { described_class.without_reserved_child_paths_regex }
+
+ it 'rejects paths containing a child reserved word' do
+ expect(subject).not_to match('hello/group_members')
+ expect(subject).not_to match('hello/activity/in-the-middle')
+ expect(subject).not_to match('foo/bar1/refs/master/logs_tree')
+ end
+
+ it 'allows a child path on the top level' do
+ expect(subject).to match('activity/foo')
+ expect(subject).to match('avatar')
+ end
+ end
+
+ describe ".valid?" do
+ it 'is not case sensitive' do
+ expect(described_class.valid?("Users")).to be_falsey
+ end
+
+ it "isn't valid when the top level is reserved" do
+ test_path = 'u/should-be-a/reserved-word'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+
+ it "isn't valid if any of the path segments is reserved" do
+ test_path = 'the-wildcard/wikis/is-not-allowed'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+
+ it "is valid if the path doesn't contain reserved words" do
+ test_path = 'there-are/no-wildcards/in-this-path'
+
+ expect(described_class.valid?(test_path)).to be_truthy
+ end
+
+ it 'allows allows a child path on the last spot' do
+ test_path = 'there/can-be-a/project-called/labels'
+
+ expect(described_class.valid?(test_path)).to be_truthy
+ end
+
+ it 'rejects a child path somewhere else' do
+ test_path = 'there/can-be-no/labels/group'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+
+ it 'rejects paths that are in an incorrect format' do
+ test_path = 'incorrect/format.git'
+
+ expect(described_class.valid?(test_path)).to be_falsey
+ end
+ end
+
+ describe '#path_reserved_for_record?' do
+ it 'reserves a sub-group named activity' do
+ group = build(:group, :nested, path: 'activity')
+
+ expect(validator.path_reserved_for_record?(group, 'activity')).to be_truthy
+ end
+
+ it "doesn't reserve a project called activity" do
+ project = build(:project, path: 'activity')
+
+ expect(validator.path_reserved_for_record?(project, 'activity')).to be_falsey
+ end
+ end
+
+ describe '#validates_each' do
+ it 'adds a message when the path is not in the correct format' do
+ group = build(:group)
+
+ validator.validate_each(group, :path, "Path with spaces, and comma's!")
+
+ expect(group.errors[:path]).to include(Gitlab::Regex.namespace_regex_message)
+ end
+
+ it 'adds a message when the path is not in the correct format' do
+ group = build(:group, path: 'users')
+
+ validator.validate_each(group, :path, 'users')
+
+ expect(group.errors[:path]).to include('users is a reserved name')
+ end
+ end
+end
diff --git a/spec/workers/expire_build_instance_artifacts_worker_spec.rb b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
index d202b3de77e..1d8da68883b 100644
--- a/spec/workers/expire_build_instance_artifacts_worker_spec.rb
+++ b/spec/workers/expire_build_instance_artifacts_worker_spec.rb
@@ -34,12 +34,14 @@ describe ExpireBuildInstanceArtifactsWorker do
context 'when associated project was removed' do
let(:build) do
create(:ci_build, :artifacts, artifacts_expiry) do |build|
- build.project.delete
+ build.project.pending_delete = true
end
end
it 'does not remove artifacts' do
- expect(build.reload.artifacts_file.exists?).to be_truthy
+ expect do
+ build.reload.artifacts_file
+ end.not_to raise_error
end
end
end
diff --git a/yarn.lock b/yarn.lock
index 8f38fb4a9a4..fdef0665d15 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -3638,7 +3638,7 @@ mime-types@^2.1.12, mime-types@~2.1.11, mime-types@~2.1.13, mime-types@~2.1.7:
dependencies:
mime-db "~1.26.0"
-mime@1.3.4, mime@^1.3.4:
+mime@1.3.4, mime@1.3.x, mime@^1.3.4:
version "1.3.4"
resolved "https://registry.yarnpkg.com/mime/-/mime-1.3.4.tgz#115f9e3b6b3daf2959983cb38f149a2d40eb5d53"
@@ -3710,6 +3710,10 @@ nested-error-stacks@^1.0.0:
dependencies:
inherits "~2.0.1"
+node-ensure@^0.0.0:
+ version "0.0.0"
+ resolved "https://registry.yarnpkg.com/node-ensure/-/node-ensure-0.0.0.tgz#ecae764150de99861ec5c810fd5d096b183932a7"
+
node-libs-browser@^1.0.0:
version "1.1.1"
resolved "https://registry.yarnpkg.com/node-libs-browser/-/node-libs-browser-1.1.1.tgz#2a38243abedd7dffcd07a97c9aca5668975a6fea"
@@ -4102,6 +4106,13 @@ pbkdf2@^3.0.3:
dependencies:
create-hmac "^1.1.2"
+pdfjs-dist@^1.8.252:
+ version "1.8.252"
+ resolved "https://registry.yarnpkg.com/pdfjs-dist/-/pdfjs-dist-1.8.252.tgz#2477245695341f7fe096824dacf327bc324c0f52"
+ dependencies:
+ node-ensure "^0.0.0"
+ worker-loader "^0.8.0"
+
pend@~1.2.0:
version "1.2.0"
resolved "https://registry.yarnpkg.com/pend/-/pend-1.2.0.tgz#7a57eb550a6783f9115331fcf4663d5c8e007a50"
@@ -5538,6 +5549,13 @@ update-notifier@0.5.0:
semver-diff "^2.0.0"
string-length "^1.0.0"
+url-loader@^0.5.8:
+ version "0.5.8"
+ resolved "https://registry.yarnpkg.com/url-loader/-/url-loader-0.5.8.tgz#b9183b1801e0f847718673673040bc9dc1c715c5"
+ dependencies:
+ loader-utils "^1.0.2"
+ mime "1.3.x"
+
url-parse@1.0.x:
version "1.0.5"
resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.0.5.tgz#0854860422afdcfefeb6c965c662d4800169927b"
@@ -5821,6 +5839,12 @@ wordwrap@~0.0.2:
version "0.0.3"
resolved "https://registry.yarnpkg.com/wordwrap/-/wordwrap-0.0.3.tgz#a3d5da6cd5c0bc0008d37234bbaf1bed63059107"
+worker-loader@^0.8.0:
+ version "0.8.0"
+ resolved "https://registry.yarnpkg.com/worker-loader/-/worker-loader-0.8.0.tgz#13582960dcd7d700dc829d3fd252a7561696167e"
+ dependencies:
+ loader-utils "^1.0.2"
+
wrap-ansi@^2.0.0:
version "2.1.0"
resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-2.1.0.tgz#d8fc3d284dd05794fe84973caecdd1cf824fdd85"